Fixed video player in app for desktop

This commit is contained in:
Mike Nolan 2024-07-28 17:59:28 -05:00
parent f81fb656e8
commit b3ae68232d
136 changed files with 9375 additions and 1126 deletions

1
.gitignore vendored
View File

@ -483,3 +483,4 @@ $RECYCLE.BIN/
# Vim temporary swap files # Vim temporary swap files
*.swp *.swp
help.txt help.txt
data/

View File

@ -1,11 +1,11 @@
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src WORKDIR /src
COPY . . COPY . .
WORKDIR /src/Tesses.CMS.Server WORKDIR /src/Tesses.CMS.Server
RUN dotnet publish -c Release -o /app RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/runtime:7.0 AS runner FROM mcr.microsoft.com/dotnet/runtime:8.0 AS runner
RUN apt update && apt install -y ffmpeg RUN apt update && apt install -y ffmpeg
WORKDIR /app WORKDIR /app
COPY --from=build /app . COPY --from=build /app .

454
Tesses.CMS.Avalonia/.gitignore vendored Normal file
View File

@ -0,0 +1,454 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# JetBrains Rider
.idea/
*.sln.iml
##
## Visual Studio Code
##
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

View File

@ -0,0 +1,6 @@
<Project>
<PropertyGroup>
<Nullable>enable</Nullable>
<AvaloniaVersion>11.0.6</AvaloniaVersion>
</PropertyGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,41 @@
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Runtime;
using Avalonia;
using Avalonia.Android;
using Avalonia.ReactiveUI;
using Tesses.VirtualFilesystem.Filesystems;
namespace Tesses.CMS.Avalonia.Android;
[Activity(
Label = "Tesses CMS",
Theme = "@style/MyTheme.NoActionBar",
Icon = "@drawable/icon",
MainLauncher = true,
ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)]
public class MainActivity : AvaloniaMainActivity<App>
{
protected override AppBuilder CustomizeAppBuilder(AppBuilder builder)
{
App.Platform = new MobilePlatform(this);
return base.CustomizeAppBuilder(builder)
.WithInterFont()
.UseReactiveUI();
}
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent? data)
{
if(requestCode == SAFFileSystem.RequestCode && resultCode == Result.Ok)
{
var res=SAFFileSystem.GetSAFFromResponsePresistant(this,data,MobilePlatform.MySAFKey);
if(res != null)
{
var appPlatform = App.Platform as MobilePlatform;
if(appPlatform != null)
appPlatform.virtualFilesystem = res;
App.Log("Openned dir");
}
}
}
}

View File

@ -0,0 +1,92 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Platform.Storage;
using Newtonsoft.Json;
using Tesses.VirtualFilesystem;
using Tesses.VirtualFilesystem.Extensions;
using Tesses.VirtualFilesystem.Filesystems;
namespace Tesses.CMS.Avalonia.Android;
internal class MobilePlatform : IPlatform
{
string configpath;
MainActivity activity;
public const string MySAFKey = "MySafKey";
public MobilePlatform(MainActivity activity)
{
this.activity = activity;
configpath=activity.FilesDir?.AbsolutePath ?? "";
if(File.Exists(Path.Combine(configpath, "tesses_cms_app.json")))
{
var conf=JsonConvert.DeserializeObject<Configuration>(File.ReadAllText(Path.Combine(configpath, "tesses_cms_app.json")));
if(conf != null)
_conf = conf;
}
var res= SAFFileSystem.GetSAFFromSharedStorage(activity,MySAFKey);
if(res != null)
virtualFilesystem = res;
}
Configuration _conf = new Configuration();
public Configuration Configuration => _conf;
public bool PlatformUsesNormalPathsForDownload => false;
internal IVirtualFilesystem? virtualFilesystem;
public IVirtualFilesystem? DownloadFilesystem => virtualFilesystem;
public async Task BrowseForDownloadDirectoryAsync()
{
/*var sp=App.Window?.StorageProvider;
if(sp != null)
{
var res=await sp.OpenFolderPickerAsync(new global::Avalonia.Platform.Storage.FolderPickerOpenOptions{Title="Browse for a downloads folder"});
if(res.Count > 0)
{
var first=res.First();
string? path=first.TryGetLocalPath();
if(!string.IsNullOrWhiteSpace(path))
{
_conf.DownloadPath = path;
await WriteConfigurationAsync();
}
}
}*/
try{
if(virtualFilesystem != null)
SAFFileSystem.Revoke(activity,MySAFKey);
}catch(Exception ex)
{
_=ex;
}
virtualFilesystem=null;
SAFFileSystem.RequestDirectory(activity,true,true);
await Task.CompletedTask;
}
public async Task ReadConfigurationAsync()
{
if(File.Exists(Path.Combine(configpath, "tesses_cms_app.json")))
{
var conf=JsonConvert.DeserializeObject<Configuration>(await File.ReadAllTextAsync(Path.Combine(configpath, "tesses_cms_app.json")));
if(conf != null)
_conf = conf;
}
}
public async Task WriteConfigurationAsync()
{
await File.WriteAllTextAsync(Path.Combine(configpath, "tesses_cms_app.json"),JsonConvert.SerializeObject(_conf));
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET" />
<application android:label="Tesses CMS" android:usesCleartextTraffic="true" android:icon="@drawable/Icon" />
</manifest>

View File

@ -0,0 +1,44 @@
Images, layout descriptions, binary blobs and string dictionaries can be included
in your application as resource files. Various Android APIs are designed to
operate on the resource IDs instead of dealing with images, strings or binary blobs
directly.
For example, a sample Android app that contains a user interface layout (main.axml),
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
would keep its resources in the "Resources" directory of the application:
Resources/
drawable/
icon.png
layout/
main.axml
values/
strings.xml
In order to get the build system to recognize Android resources, set the build action to
"AndroidResource". The native Android APIs do not operate directly with filenames, but
instead operate on resource IDs. When you compile an Android application that uses resources,
the build system will package the resources for distribution and generate a class called "R"
(this is an Android convention) that contains the tokens for each one of the resources
included. For example, for the above Resources layout, this is what the R class would expose:
public class R {
public class drawable {
public const int icon = 0x123;
}
public class layout {
public const int main = 0x456;
}
public class strings {
public const int first_string = 0xabc;
public const int second_string = 0xbcd;
}
}
You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main
to reference the layout/main.axml file, or R.strings.first_string to reference the first
string in the dictionary file values/strings.xml.

View File

@ -0,0 +1,66 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="1000"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

View File

@ -0,0 +1,71 @@
<animated-vector
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt">
<aapt:attr name="android:drawable">
<vector
android:name="vector"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<group
android:name="wrapper"
android:translateX="21"
android:translateY="21">
<group android:name="group">
<path
android:name="path"
android:pathData="M 74.853 85.823 L 75.368 85.823 C 80.735 85.823 85.144 81.803 85.761 76.602 L 85.836 41.76 C 85.225 18.593 66.254 0 42.939 0 C 19.24 0 0.028 19.212 0.028 42.912 C 0.028 66.357 18.831 85.418 42.18 85.823 L 74.853 85.823 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
<path
android:name="path_1"
android:pathData="M 43.059 14.614 C 29.551 14.614 18.256 24.082 15.445 36.743 C 18.136 37.498 20.109 39.968 20.109 42.899 C 20.109 45.831 18.136 48.301 15.445 49.055 C 18.256 61.716 29.551 71.184 43.059 71.184 C 47.975 71.184 52.599 69.93 56.628 67.723 L 56.628 70.993 L 71.344 70.993 L 71.344 44.072 C 71.357 43.714 71.344 43.26 71.344 42.899 C 71.344 27.278 58.68 14.614 43.059 14.614 Z M 29.51 42.899 C 29.51 35.416 35.576 29.35 43.059 29.35 C 50.541 29.35 56.607 35.416 56.607 42.899 C 56.607 50.382 50.541 56.448 43.059 56.448 C 35.576 56.448 29.51 50.382 29.51 42.899 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"
android:fillType="evenOdd"/>
<path
android:name="path_2"
android:pathData="M 18.105 42.88 C 18.105 45.38 16.078 47.407 13.579 47.407 C 11.079 47.407 9.052 45.38 9.052 42.88 C 9.052 40.381 11.079 38.354 13.579 38.354 C 16.078 38.354 18.105 40.381 18.105 42.88 Z"
android:fillColor="#00ffffff"
android:strokeWidth="1"/>
</group>
</group>
</vector>
</aapt:attr>
<target android:name="path_2">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:duration="500"
android:valueFrom="#00ffffff"
android:valueTo="#f9f9fb"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
<target android:name="path_1">
<aapt:attr name="android:animation">
<objectAnimator
android:propertyName="fillColor"
android:startOffset="100"
android:duration="900"
android:valueFrom="#00ffffff"
android:valueTo="#161c2d"
android:valueType="colorType"
android:interpolator="@android:interpolator/fast_out_slow_in"/>
</aapt:attr>
</target>
</animated-vector>

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<color android:color="@color/splash_background"/>
</item>
<item android:drawable="@drawable/icon"
android:width="120dp"
android:height="120dp"
android:gravity="center" />
</layer-list>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#212121</color>
</resources>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowSplashScreenBackground">@color/splash_background</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avalonia_anim</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
<item name="postSplashScreenTheme">@style/MyTheme.Main</item>
</style>
<style name="MyTheme.Main"
parent ="MyTheme.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
</style>
</resources>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="splash_background">#FFFFFF</color>
</resources>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="MyTheme">
</style>
<style name="MyTheme.NoActionBar" parent="@style/Theme.AppCompat.DayNight.NoActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowBackground">@drawable/splash_screen</item>
<item name="android:windowNoTitle">true</item>
</style>
</resources>

View File

@ -0,0 +1,531 @@
using Android.Content;
using Tesses.VirtualFilesystem;
using Android.App;
using Android.Content.PM;
using AndroidX.DocumentFile.Provider;
using Tesses.VirtualFilesystem.Extensions;
using System.Collections.Generic;
using System;
using System.IO;
using Tesses.CMS.Avalonia;
namespace Tesses.VirtualFilesystem.Filesystems;
public class SAFFileSystem : SyncFileSystem
{
public const int RequestCode = 1447775022;
public const string SharedPreferencesFile = "tesess_vfs";
public static void RequestDirectory(Activity app,bool canwrite=true,bool presistant=false,int reqCode=RequestCode)
{
Intent intent = new Intent(Intent.ActionOpenDocumentTree);
intent.AddFlags(ActivityFlags.GrantReadUriPermission|(canwrite? ActivityFlags.GrantWriteUriPermission : 0)|(presistant?ActivityFlags.GrantPersistableUriPermission:0));
app.StartActivityForResult(intent,reqCode);
}
public static SAFFileSystem? GetSAFFromResponse(Context? app,Intent? intent)
{
if(app == null || intent == null) return null;
var uri=intent.Data;
if(uri != null)
{
return new SAFFileSystem(uri,app);
}
return null;
}
public static SAFFileSystem? GetSAFFromResponsePresistant(Context? app,Intent? intent,string key)
{
if(app == null || intent == null) return null;
var uri=intent.Data;
if(uri != null)
{
var r = app?.ContentResolver;
if(r == null) return null;
var rw = intent.Flags & (ActivityFlags.GrantReadUriPermission | ActivityFlags.GrantWriteUriPermission);
r.TakePersistableUriPermission(uri,rw);
var app2=app?.CreatePackageContext(app.PackageName,0);
var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private);
sharedPrefs?.Edit()?.PutString(key,uri.ToString())?.Apply();
return new SAFFileSystem(uri,app);
}
return null;
}
public static void Revoke(Context? app,string key)
{
var app2=app?.CreatePackageContext(app.PackageName,0);
var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private);
var res=sharedPrefs?.GetString(key,null);
var r = app?.ContentResolver;
if(r == null) return;
if(string.IsNullOrWhiteSpace(res)) return;
var uri = Android.Net.Uri.Parse(res);
if(uri == null) return;
r.ReleasePersistableUriPermission(uri,ActivityFlags.GrantReadUriPermission);
sharedPrefs?.Edit()?.Remove(key)?.Apply();
}
public static SAFFileSystem? GetSAFFromSharedStorage(Context? app,string key)
{
if(app == null) return null;
var app2=app?.CreatePackageContext(app.PackageName,0);
var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private);
var res=sharedPrefs?.GetString(key,null);
if(string.IsNullOrWhiteSpace(res)) return null;
var uri = Android.Net.Uri.Parse(res);
if(uri == null) return null;
return new SAFFileSystem(uri,app);
}
public SAFFileSystem(global::Android.Net.Uri uri,Context? ctx)
{
Uri = uri;
Context = ctx;
}
public global::Android.Net.Uri Uri {get;set;}
public Context? Context {get;set;}
public override void CreateDirectory(UnixPath directory)
{
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) return;
foreach(var item in directory.Parts)
{
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
{
dir = dir?.CreateDirectory(item);
}
else if(dir2.IsDirectory)
{
dir = dir2;
}
}
else
{
dir = dir?.CreateDirectory(item);
}
}
}
}
public override void DeleteDirectory(UnixPath path)
{
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) return;
for(int i = 0;i<path.Parts.Length;i++)
{
var item = path.Parts[i];
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
{
return;
}
else if(dir2.IsDirectory)
{
dir = dir2;
if(i==path.Parts.Length -1)
dir.Delete();
}
}
}
}
}
public override void DeleteFile(UnixPath path)
{
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) return;
for(int i = 0;i<path.Parts.Length;i++)
{
var item = path.Parts[i];
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
{
return;
}
else if(dir2.IsDirectory)
{
dir=dir2;
}
else if(dir2.IsFile)
{
dir = dir2;
if(i==path.Parts.Length -1)
dir.Delete();
}
}
}
}
}
public override bool DirectoryExists(UnixPath path)
{
if(path.IsRoot) return true;
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) return false;
for(int i = 0;i<path.Parts.Length;i++)
{
var item = path.Parts[i];
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
{
return false;
}
else if(dir2.IsDirectory)
{
dir = dir2;
if(i==path.Parts.Length -1)
return true;
}
}
}
}
return false;
}
public override IEnumerable<UnixPath> EnumerateFileSystemEntries(UnixPath path)
{
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) yield break;
if(path.IsRoot)
{
foreach(var _item in dir.ListFiles())
{
yield return path / _item.Name;
}
yield break;
}
for(int i = 0;i<path.Parts.Length;i++)
{
var item = path.Parts[i];
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
{
yield break;
}
else if(dir2.IsDirectory)
{
dir = dir2;
if(i==path.Parts.Length -1)
{
foreach(var _item in dir2.ListFiles())
{
yield return path / _item.Name;
}
}
}
}
}
}
}
public override bool FileExists(UnixPath path)
{
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) return false;
for(int i = 0;i<path.Parts.Length;i++)
{
var item = path.Parts[i];
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
{
return false;
}
else if(dir2.IsDirectory)
{
dir=dir2;
}
else if(dir2.IsFile)
{
dir = dir2;
if(i==path.Parts.Length -1)
return true;
}
}
}
}
return false;
}
public override DateTime GetCreationTime(UnixPath path)
{
return DateTime.Now;
}
public override DateTime GetLastAccessTime(UnixPath path)
{
return DateTime.Now;
}
public override DateTime GetLastWriteTime(UnixPath path)
{
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) return DateTime.Now;
for(int i = 0;i<path.Parts.Length;i++)
{
var item = path.Parts[i];
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(dir2.Exists())
{
dir = dir2;
if(i==path.Parts.Length -1)
return DateTimeOffset.FromUnixTimeSeconds(dir2.LastModified()).DateTime;
}
else return DateTime.Now;
}
}
}
return DateTime.Now;
}
public override void MoveDirectory(UnixPath src, UnixPath dest)
{
foreach(var item in this.EnumerateDirectories(src))
{
MoveDirectory(item,dest / item.Name);
}
foreach(var item in this.EnumerateFiles(src))
{
MoveFile(item,dest / item.Name);
}
DeleteDirectory(src);
}
public override void MoveFile(UnixPath src, UnixPath dest)
{
if(src.Parent == dest.Parent)
{
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) return;
for(int i = 0;i<src.Parts.Length;i++)
{
var item = src.Parts[i];
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual)
{
return;
}
else if(dir2.IsDirectory)
{
dir=dir2;
}
else if(dir2.IsFile)
{
dir = dir2;
if(i==src.Parts.Length -1)
dir.RenameTo(dest.Name);
}
}
}
}
}
else
{
this.CopyFile(src,dest);
DeleteFile(src);
}
}
public override Stream Open(UnixPath path, FileMode mode, FileAccess access, FileShare share)
{
if(Context != null && Uri != null)
{
var dir = DocumentFile.FromTreeUri(Context,Uri);
if(dir == null) throw new FileNotFoundException(path.ToString());
int j=0;
for(int i = 0;i<path.Parts.Length;i++)
{
j=i;
var item = path.Parts[i];
var dir2= dir?.FindFile(item);
if(dir2 != null)
{
if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual && i < path.Parts.Length - 1)
{
throw new FileNotFoundException(path.ToString());
}
else if(dir2.IsDirectory)
{
dir = dir2;
}
if(i==path.Parts.Length -1)
{
if(dir2?.IsFile ?? false)
{
if(mode == FileMode.CreateNew) throw new IOException("File already exists");
return _OpenFile(dir2.Uri,mode,access,share);
}
else
{
var (mimeType,displayName)=GetDisplayNameAndMime(item);
var f=dir?.CreateFile(mimeType,displayName);
if(mode == FileMode.Open || mode == FileMode.Truncate || mode == FileMode.Append)
throw new IOException("File does not exist");
if(f != null)
return _OpenFile(f.Uri,mode,access,share);
throw new IOException("Could not open file");
}
}
}
else{
var (mimeType,displayName)=GetDisplayNameAndMime(item);
var f=dir?.CreateFile(mimeType,displayName);
if(mode == FileMode.Open || mode == FileMode.Truncate || mode == FileMode.Append)
throw new IOException("File does not exist");
if(f != null)
return _OpenFile(f.Uri,mode,access,share);
throw new IOException("Could not open file");
}
}
}
throw new FileNotFoundException(path.ToString());
}
private (string mimeType, string displayName) GetDisplayNameAndMime(string item)
{
var mime=HeyRed.Mime.MimeTypesMap.GetMimeType(item);
if(mime == "application/octet-stream")
{
return ("unknown/unknown",item);
}
else
{
return (mime,Path.GetFileNameWithoutExtension(item));
}
}
private Stream _OpenFile(global::Android.Net.Uri uri, FileMode mode, FileAccess access, FileShare share)
{
//FileMode.Append -> "a"
//FileMode.Create -> "w"
//FileMode.CreateNew -> "w"
//FileMode.Open -> "r or w"
//FileMode.OpenOrCreate -> "r or w"
//FileMode.Truncate -> "t"
//"r", "w", "wt", "wa", "rw" or "rwt"
Stream? strm = null;
if(access == FileAccess.Read)
strm = Context?.ContentResolver?.OpenInputStream(uri) ?? null;
if(access == FileAccess.Write)
if(mode == FileMode.Truncate)
strm = Context?.ContentResolver?.OpenOutputStream(uri,"wt");
else if(mode == FileMode.Append)
strm = Context?.ContentResolver?.OpenOutputStream(uri,"wa");
else
strm = Context?.ContentResolver?.OpenOutputStream(uri,"w");
if(access == FileAccess.ReadWrite)
if(mode == FileMode.Truncate)
strm = Context?.ContentResolver?.OpenOutputStream(uri,"rwt");
else
strm = Context?.ContentResolver?.OpenOutputStream(uri,"rw");
if(strm == null) throw new IOException("Failed to open stream");
return strm;
}
public override void SetCreationTime(UnixPath path, DateTime time)
{
}
public override void SetLastAccessTime(UnixPath path, DateTime time)
{
}
public override void SetLastWriteTime(UnixPath path, DateTime time)
{
}
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-android</TargetFramework>
<SupportedOSPlatformVersion>21</SupportedOSPlatformVersion>
<Nullable>enable</Nullable>
<ApplicationId>com.CompanyName.Tesses.CMS.Avalonia</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat>
<AndroidEnableProfiledAot>False</AndroidEnableProfiledAot>
</PropertyGroup>
<ItemGroup>
<AndroidResource Include="Icon.png">
<Link>Resources\drawable\Icon.png</Link>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Android" Version="$(AvaloniaVersion)" />
<PackageReference Include="MimeTypesMap" Version="1.0.8" />
<PackageReference Include="Xamarin.AndroidX.Core.SplashScreen" Version="1.0.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tesses.CMS.Avalonia\Tesses.CMS.Avalonia.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,95 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using LibVLCSharp.Avalonia;
using LibVLCSharp.Shared;
using Newtonsoft.Json;
using Tesses.VirtualFilesystem;
using Tesses.VirtualFilesystem.Extensions;
using Tesses.VirtualFilesystem.Filesystems;
namespace Tesses.CMS.Avalonia.Desktop;
internal class DesktopPlatform : IPlatform
{
public DesktopPlatform()
{
if(rootfs.FileExists(Special.LocalAppData / "tesses_cms_app.json"))
{
var conf=JsonConvert.DeserializeObject<Configuration>(rootfs.ReadAllText(Special.LocalAppData/"tesses_cms_app.json"));
if(conf != null)
_conf = conf;
var path=UnixPath.FromLocal(_conf.DownloadPath);
virtualFilesystem = rootfs.GetSubdirFilesystem(path);
}
}
LocalFileSystem rootfs=new LocalFileSystem();
Configuration _conf = new Configuration();
public Configuration Configuration => _conf;
public bool PlatformUsesNormalPathsForDownload => true;
IVirtualFilesystem? virtualFilesystem;
public IVirtualFilesystem? DownloadFilesystem => virtualFilesystem;
public async Task BrowseForDownloadDirectoryAsync()
{
var sp=App.Window?.StorageProvider;
if(sp != null)
{
var res=await sp.OpenFolderPickerAsync(new global::Avalonia.Platform.Storage.FolderPickerOpenOptions{Title="Browse for a downloads folder"});
if(res.Count > 0)
{
var first=res.First();
string? path=first.TryGetLocalPath();
if(!string.IsNullOrWhiteSpace(path))
{
_conf.DownloadPath = path;
await WriteConfigurationAsync();
}
}
}
}
public async Task ReadConfigurationAsync()
{
if(await rootfs.FileExistsAsync(Special.LocalAppData / "tesses_cms_app.json"))
{
var conf=JsonConvert.DeserializeObject<Configuration>(await rootfs.ReadAllTextAsync(Special.LocalAppData/"tesses_cms_app.json"));
if(conf != null)
_conf = conf;
}
}
public async Task WriteConfigurationAsync()
{
await rootfs.WriteAllTextAsync(Special.LocalAppData / "tesses_cms_app.json",JsonConvert.SerializeObject(_conf));
var path=UnixPath.FromLocal(_conf.DownloadPath);
virtualFilesystem = rootfs.GetSubdirFilesystem(path);
}
public Control CreatePlayer()
{
return new VideoPlayer();
}
public void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer)
{
var ctrl = control as VideoPlayer;
if(ctrl != null)
ctrl.MediaPlayer = mediaPlayer;
}
public MediaPlayer? GetMediaPlayer(Control control)
{
var ctrl = control as VideoPlayer;
if(ctrl != null)
return ctrl.MediaPlayer;
return null;
}
}

View File

@ -0,0 +1,28 @@
using System;
using Avalonia;
using Avalonia.ReactiveUI;
namespace Tesses.CMS.Avalonia.Desktop;
sealed class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) {
App.Platform = new DesktopPlatform();
BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.UseReactiveUI();
}

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<!--If you are willing to use Windows/MacOS native APIs you will need to create 3 projects.
One for Windows with net8.0-windows TFM, one for MacOS with net8.0-macos and one with net8.0 TFM for Linux.-->
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="LibVlcSharp.Avalonia" Version="3.8.5" />
<PackageReference Include="Tesses.VirtualFilesystem" Version="1.0.2" />
<PackageReference Include="Tesses.VirtualFilesystem.Base" Version="1.0.2" />
<PackageReference Include="Tesses.Virtualfilesystem.Local" Version="1.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tesses.CMS.Avalonia\Tesses.CMS.Avalonia.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,124 @@
using System;
using System.IO;
using Avalonia.Controls;
using Avalonia.Threading;
using LibVLCSharp.Avalonia;
using LibVLCSharp.Shared;
using Tesses.CMS.Avalonia;
public class VideoPlayer : Grid
{
public VideoPlayer()
{
view=new VideoView();
this.RowDefinitions.Add(new RowDefinition(GridLength.Star));
this.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
this.Children.Add(view);
SetRow(view,0);
Grid seekPanel = new Grid();
Button ss=new Button();
ss.Content = "SS";
ss.Click += (sender,e)=>{
//take screenshot
string realDir=App.Platform.DownloadFilesystem == null ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : Path.Combine(App.Platform.Configuration.DownloadPath,"Screenshots");
Directory.CreateDirectory(realDir);
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");
MediaPlayer.TakeSnapshot(0,name,0,0);
};
slider = new Slider();
slider.Minimum = 0;
slider.Maximum = 1000000;
slider.ValueChanged+= (sender,e)=>{
if(MediaPlayer != null)
{
if(!theyareseeking)
MediaPlayer.Position = (float)(slider.Value/slider.Maximum);
}
};
playBtn=new Button();
playBtn.Content = "Play";
playBtn.Click += (sender,e)=>{
if(MediaPlayer != null)
{
if(MediaPlayer.IsPlaying)
{
MediaPlayer.Pause();
}
else
{
MediaPlayer.Play();
}
}
};
seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star));
seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
seekPanel.Children.Add(playBtn);
seekPanel.Children.Add(slider);
seekPanel.Children.Add(ss);
SetColumn(playBtn,0);
SetColumn(slider,1);
SetColumn(ss,2);
this.Children.Add(seekPanel);
SetRow(seekPanel,1);
}
Slider slider;
Button playBtn;
VideoView view;
public MediaPlayer? MediaPlayer
{
get=>view.MediaPlayer;
set{
if(view.MediaPlayer != null)
{
view.MediaPlayer.Paused -= Paused;
view.MediaPlayer.Playing -= Playing;
view.MediaPlayer.PositionChanged -= PositionChanged;
}
view.MediaPlayer=value;
if(view.MediaPlayer != null)
{
view.MediaPlayer.Paused += Paused;
view.MediaPlayer.Playing += Playing;
view.MediaPlayer.PositionChanged += PositionChanged;
}
}
}
bool theyareseeking=false;
private void PositionChanged(object? sender, MediaPlayerPositionChangedEventArgs e)
{
Dispatcher.UIThread.Invoke(()=>{
theyareseeking=true;
slider.Value = (e.Position*slider.Maximum);
theyareseeking=false;
});
}
private void Playing(object? sender, EventArgs e)
{
Dispatcher.UIThread.Invoke(()=>{
playBtn.Content="Pause";
});
}
private void Paused(object? sender, EventArgs e)
{
Dispatcher.UIThread.Invoke(()=>{
playBtn.Content="Play";
});
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Tesses.CMS.Avalonia.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@ -0,0 +1,50 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32811.315
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Avalonia", "Tesses.CMS.Avalonia\Tesses.CMS.Avalonia.csproj", "{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Avalonia.Desktop", "Tesses.CMS.Avalonia.Desktop\Tesses.CMS.Avalonia.Desktop.csproj", "{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Avalonia.Android", "Tesses.CMS.Avalonia.Android\Tesses.CMS.Avalonia.Android.csproj", "{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DA99C4E-89E3-4049-9C22-0A7EC60D83D8}"
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.Build.0 = Release|Any CPU
{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.Build.0 = Release|Any CPU
{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.Build.0 = Release|Any CPU
{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.Build.0 = Release|Any CPU
{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {83CB65B8-011F-4ED7-BCD3-A6CFA935EF7E}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,15 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Tesses.CMS.Avalonia"
x:Class="Tesses.CMS.Avalonia.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

View File

@ -0,0 +1,160 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Tesses.CMS.Avalonia.ViewModels;
using Tesses.CMS.Avalonia.Views;
using Tesses.CMS.Client;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Tesses.VirtualFilesystem;
using Tesses.VirtualFilesystem.Extensions;
using System.IO;
namespace Tesses.CMS.Avalonia;
public partial class App : Application
{
public static IPlatform Platform {get;set;} = new NullPlatform();
public static MainWindow? Window {get;set;}
public static TessesCMSClient Client {get;} = new TessesCMSClient();
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
Client.RootUrl = Platform.Configuration.ServerUrl;
string title = GetApplicationTitle();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
Window = new MainWindow
{
DataContext = new MainViewModel(title),
Title = title
};
desktop.MainWindow = Window;
}
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
{
singleViewPlatform.MainView = new MainView
{
DataContext = new MainViewModel(title)
};
}
base.OnFrameworkInitializationCompleted();
}
internal static string GetApplicationTitle()
{
string title="Tesses CMS";
Task.Run(async()=>{
try {
var data = await Client.GetBrandingAsync();
title = data.Title;
}catch(Exception ex)
{
_=ex;
}
}).Wait();
return title;
}
internal static async Task<IImage> GetMovieThumbnailAsync(string username, string name)
{
//we need to cache the resource
if(Platform.Configuration.CacheResources && Platform.DownloadFilesystem != null)
{
UnixPath cacheDir = Special.Root / "Metadata" / "Cache" / username / "Movies" / 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.Movies.GetMovieContentMetadataAsync(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.Movies.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.Movies.GetMovieContentMetadataAsync(username,name);
if(metadata.HasThumbnail)
{
MemoryStream ms=new MemoryStream();
Log($"About to read from network: {username} {name}");
await Client.Movies.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)
{
lock(App.Platform)
{
var fs=App.Platform.DownloadFilesystem;
if(fs != null && App.Platform.Configuration.Log)
{
using(var strm = fs.Open(Special.Root/"Logs.txt",FileMode.Append,FileAccess.Write,FileShare.None))
{
using(var sw = new StreamWriter(strm))
{
sw.WriteLine(text);
}
}
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,12 @@
public class Configuration
{
public string ServerUrl {get;set;}="https://tessesstudios.com/";
public string LoginToken {get;set;}="";
public string DownloadPath {get;set;}="";
public bool CacheResources {get;set;}=true;
public bool Log {get;set;}=true;
}

View File

@ -0,0 +1,70 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using LibVLCSharp.Shared;
using Tesses.VirtualFilesystem;
public interface IPlatform
{
Configuration Configuration {get;}
Task BrowseForDownloadDirectoryAsync();
bool PlatformUsesNormalPathsForDownload {get;}
IVirtualFilesystem? DownloadFilesystem {get;}
Task WriteConfigurationAsync();
Task ReadConfigurationAsync();
Control CreatePlayer();
void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer);
MediaPlayer? GetMediaPlayer(Control control);
}
public class NullPlatform : IPlatform
{
public bool PlatformUsesNormalPathsForDownload => false;
public IVirtualFilesystem? DownloadFilesystem
{
get => null;
set => _=value;
}
Configuration _conf=new Configuration();
public Configuration Configuration => _conf;
public async Task BrowseForDownloadDirectoryAsync()
{
await Task.CompletedTask;
}
public async Task ReadConfigurationAsync()
{
await Task.CompletedTask;
}
public async Task WriteConfigurationAsync()
{
await Task.CompletedTask;
}
public Control CreatePlayer()
{
return new TextBlock(){Text="Video player unavailable"};
}
public void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer)
{
}
public MediaPlayer? GetMediaPlayer(Control control)
{
return null;
}
}

View File

@ -0,0 +1,16 @@
using Avalonia.Media;
using Tesses.CMS.Client;
namespace Tesses.CMS.Avalonia.Models;
public class MovieItem
{
public MovieItem(Movie movie,IImage image)
{
Movie = movie;
Image = image;
}
public Movie Movie {get;}
public IImage Image {get;}
public string Name => Movie.ProperName;
}

View File

@ -0,0 +1,16 @@
using System;
using Tesses.CMS.Avalonia.ViewModels;
namespace Tesses.CMS.Avalonia.Models;
public class Page
{
public Page(string name,Func<ViewModelBase> model)
{
Name = name;
getViewModel=model;
}
public string Name {get;set;}
private Func<ViewModelBase> getViewModel;
public ViewModelBase ViewModel => getViewModel();
}

View File

@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="LibVLCSharp" Version="3.8.5" />
<PackageReference Include="Tesses.VirtualFilesystem" Version="1.0.2" />
<PackageReference Include="Tesses.VirtualFilesystem.Base" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Tesses.CMS.Client\Tesses.CMS.Client.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,30 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Tesses.CMS.Avalonia.ViewModels;
namespace Tesses.CMS.Avalonia;
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
return null;
var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object? data)
{
return data is ViewModelBase;
}
}

View File

@ -0,0 +1,19 @@
namespace Tesses.CMS.Avalonia.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using LibVLCSharp.Shared;
public partial class DownloadsPageViewModel : ViewModelBase
{
public DownloadsPageViewModel()
{
LibVLC vlc=new LibVLC("--input-repeat=65535");
Player=new MediaPlayer(new Media(vlc,"https://tytdarchive.site.tesses.net/content/PreMuxed/PzUKeGZiEl0.mp4",FromType.FromLocation));
}
[ObservableProperty]
private MediaPlayer? _player;
}

View File

@ -0,0 +1,8 @@
namespace Tesses.CMS.Avalonia.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
public partial class FavoritesPageViewModel : ViewModelBase
{
}

View File

@ -0,0 +1,29 @@
namespace Tesses.CMS.Avalonia.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Avalonia.ViewModels.HomePages;
public partial class HomePageViewModel : ViewModelBase
{
public HomePageViewModel()
{
_currentPage = new HomeUserListPageViewModel(this);
}
[ObservableProperty]
private ViewModelBase _currentPage;
[RelayCommand]
private void Back()
{
var page = CurrentPage as IBackable;
if(page != null)
{
CurrentPage = page.Back();
}
else
{
//can't go back
}
}
}

View File

@ -0,0 +1,53 @@
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 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)
{
App.Log("In HomeMovieListPageViewModel::ctor block begin");
this.homePage = homePage;
Task.Run(async()=>{
App.Log("In HomeMovieListPageViewModel::ctor::async block begin");
await foreach(var item in App.Client.Movies.GetMoviesAsync(homePage.Username))
{
try{
_movies.Add(new MovieItem(item,await App.GetMovieThumbnailAsync(homePage.Username,item.Name)));
}catch(Exception ex)
{
App.Log(ex.ToString());
}
}
App.Log("In HomeMovieListPageViewModel::ctor::async block end");
}).Wait(0);
App.Log("In HomeMovieListPageViewModel::ctor block end");
}
[ObservableProperty]
private ObservableCollection<MovieItem> _movies=new ObservableCollection<MovieItem>();
[ObservableProperty]
private MovieItem? _selectedListItem;
partial void OnSelectedListItemChanged(MovieItem? value)
{
if (value is null) return;
SelectedListItem=null;
//homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value);
}
public ViewModelBase Back()
{
return homePage;
}
}

View File

@ -0,0 +1,37 @@
namespace Tesses.CMS.Avalonia.ViewModels.HomePages;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Avalonia.Views.HomePages;
using Tesses.CMS.Client;
public partial class HomeUserListPageViewModel : ViewModelBase
{
HomePageViewModel homePage;
public HomeUserListPageViewModel(HomePageViewModel homePage)
{
this.homePage = homePage;
Task.Run(async()=>{
await foreach(var item in App.Client.Users.GetUsersAsync())
{
App.Log($"Got user {item}");
_users.Add(item);
}
}).Wait(0);
}
[ObservableProperty]
private ObservableCollection<UserAccount> _users=new ObservableCollection<UserAccount>();
[ObservableProperty]
private UserAccount? _selectedListItem;
partial void OnSelectedListItemChanged(UserAccount? value)
{
if (value is null) return;
App.Log($"Selected account {value.ProperName}");
SelectedListItem=null;
homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value);
}
}

View File

@ -0,0 +1,55 @@
namespace Tesses.CMS.Avalonia.ViewModels.HomePages;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Client;
public class UserPageItem
{
public UserPageItem(string name,ViewModelBase vmb)
{
Name = name;
Item = vmb;
}
public string Name {get;set;}
public ViewModelBase Item {get;set;}
}
public partial class HomeUserPageViewModel : ViewModelBase, IBackable
{
HomePageViewModel homePage;
HomeUserListPageViewModel userList;
UserAccount account;
public string Username => account.Username;
[ObservableProperty]
private ObservableCollection<UserPageItem> _userItems=new ObservableCollection<UserPageItem>();
public HomeUserPageViewModel(HomePageViewModel homePage,HomeUserListPageViewModel userList, UserAccount account)
{
this.homePage = homePage;
this.userList = userList;
this.account = account;
UserItems.Add(new UserPageItem("Movies",new HomeMovieListPageViewModel(this)));
}
[ObservableProperty]
private UserPageItem? _selectedListItem;
partial void OnSelectedListItemChanged(UserPageItem? value)
{
if (value is null) return;
SelectedListItem=null;
homePage.CurrentPage = value.Item;
}
public ViewModelBase Back()
{
return userList;
}
}

View File

@ -0,0 +1,6 @@
namespace Tesses.CMS.Avalonia.ViewModels;
public interface IBackable
{
public ViewModelBase Back();
}

View File

@ -0,0 +1,60 @@
namespace Tesses.CMS.Avalonia.ViewModels;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Tesses.CMS.Avalonia.Models;
public partial class MainViewModel : ViewModelBase
{
public MainViewModel(string title)
{
_pages.Add(new Page("Settings",()=>new SettingsPageViewModel(this)));
SelectedListItem = Pages.First();
this.Title = title;
}
[ObservableProperty]
private bool _paneOpen=true;
[ObservableProperty]
private ViewModelBase _currentPage = new HomePageViewModel();
[ObservableProperty]
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]
private Page? _selectedListItem;
[ObservableProperty]
private string _loginText="Login";
[ObservableProperty]
private string _title;
partial void OnSelectedListItemChanged(Page? value)
{
if (value is null) return;
CurrentPage = value.ViewModel;
}
[RelayCommand]
private void TogglePane()
{
PaneOpen = !PaneOpen;
}
[RelayCommand]
private void LoginAccount()
{
App.Log("Login button");
}
}

View File

@ -0,0 +1,8 @@
namespace Tesses.CMS.Avalonia.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
public partial class NotificationsPageViewModel : ViewModelBase
{
}

View File

@ -0,0 +1,50 @@
namespace Tesses.CMS.Avalonia.ViewModels;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
public partial class SettingsPageViewModel : ViewModelBase
{
MainViewModel mvm;
public SettingsPageViewModel(MainViewModel mvm)
{
this.mvm = mvm;
}
[RelayCommand]
private async Task Browse()
{
await App.Platform.BrowseForDownloadDirectoryAsync();
if(App.Platform.PlatformUsesNormalPathsForDownload)
Path = App.Platform.Configuration.DownloadPath;
}
[RelayCommand]
private async Task Save()
{
if(App.Platform.PlatformUsesNormalPathsForDownload)
App.Platform.Configuration.DownloadPath = Path;
App.Platform.Configuration.ServerUrl = Url;
App.Platform.Configuration.Log = Log;
App.Platform.Configuration.CacheResources = CacheResources;
await App.Platform.WriteConfigurationAsync();
App.Client.RootUrl = App.Platform.Configuration.ServerUrl;
string title=App.GetApplicationTitle();
mvm.Title = title;
if(App.Window != null)
App.Window.Title = title;
}
[ObservableProperty]
private string _path = App.Platform.PlatformUsesNormalPathsForDownload ? App.Platform.Configuration.DownloadPath : "<Not Editable>";
[ObservableProperty]
private string _url = App.Platform.Configuration.ServerUrl;
[ObservableProperty]
private bool _log = App.Platform.Configuration.Log;
[ObservableProperty]
private bool _cacheResources = App.Platform.Configuration.CacheResources;
}

View File

@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
using ReactiveUI;
namespace Tesses.CMS.Avalonia.ViewModels;
public class ViewModelBase : ObservableObject
{
}

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"
xmlns:v="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.DownloadsPageView"
x:DataType="vm:DownloadsPageViewModel">
<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>
<v:VideoPlayerWrapper MediaPlayer="{Binding Player}" />
</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 DownloadsPageView : UserControl
{
public DownloadsPageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,15 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.FavoritesPageView"
x:DataType="vm:FavoritesPageViewModel">
<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:FavoritesPageViewModel />
</Design.DataContext>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Tesses.CMS.Avalonia.Views;
public partial class FavoritesPageView : UserControl
{
public FavoritesPageView()
{
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.HomeMovieListPageView"
x:DataType="vm:HomeMovieListPageViewModel">
<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 Movies}" 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 HomeMovieListPageView : UserControl
{
public HomeMovieListPageView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,18 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.HomePageView"
x:DataType="vm:HomePageViewModel">
<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:HomePageViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*">
<Button Grid.Row="0" Command="{Binding BackCommand}">&lt;-Back</Button>
<TransitioningContentControl Grid.Row="1" Content="{Binding CurrentPage}" />
</Grid>
</UserControl>

View File

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

View File

@ -0,0 +1,23 @@
<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.HomeUserListPageView"
x:DataType="vm:HomeUserListPageViewModel">
<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:HomeUserListPageViewModel />
</Design.DataContext>
<ListBox ItemsSource="{Binding Users}" SelectedItem="{Binding SelectedListItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ProperName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>

View File

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

View File

@ -0,0 +1,23 @@
<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.HomeUserPageView"
x:DataType="vm:HomeUserPageViewModel">
<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:HomeUserPageViewModel />
</Design.DataContext>
<ListBox ItemsSource="{Binding UserItems}" SelectedItem="{Binding SelectedListItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>

View File

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

View File

@ -0,0 +1,44 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.MainView"
x:DataType="vm:MainViewModel">
<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:MainViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,*">
<Grid ColumnDefinitions="Auto,*,Auto" Grid.Row="0" Background="Green">
<Button Grid.Column="0" Command="{Binding TogglePaneCommand}">Menu</Button>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="1" Text="{Binding Title}" />
<Button Grid.Column="2" Command="{Binding LoginAccountCommand}"><TextBlock Text="{Binding LoginText}"></TextBlock></Button>
</Grid>
<SplitView Grid.Row="1" IsPaneOpen="{Binding PaneOpen}"
DisplayMode="Inline"
OpenPaneLength="200">
<SplitView.Pane>
<StackPanel>
<ListBox ItemsSource="{Binding Pages}" SelectedItem="{Binding SelectedListItem}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</SplitView.Pane>
<SplitView.Content>
<TransitioningContentControl Content="{Binding CurrentPage}" />
</SplitView.Content>
</SplitView>
</Grid>
</UserControl>

View File

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

View File

@ -0,0 +1,12 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Tesses.CMS.Avalonia.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:views="clr-namespace:Tesses.CMS.Avalonia.Views"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.MainWindow"
Icon="/Assets/avalonia-logo.ico"
Title="Tesses CMS">
<views:MainView />
</Window>

View File

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

View File

@ -0,0 +1,15 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.NotificationsPageView"
x:DataType="vm:NotificationsPageViewModel">
<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:NotificationsPageViewModel />
</Design.DataContext>
</UserControl>

View File

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

View File

@ -0,0 +1,34 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Tesses.CMS.Avalonia.Views.SettingsPageView"
x:DataType="vm:SettingsPageViewModel">
<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:SettingsPageViewModel />
</Design.DataContext>
<StackPanel>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Grid.Column="0" Text="Endpoint: "/>
<TextBox Text="{Binding Url}" Grid.Column="1" Watermark="Endpoint"/>
</Grid>
<Grid ColumnDefinitions="Auto,*,Auto">
<TextBlock Grid.Column="0" Text="Download Path: "/>
<TextBox Grid.Column="1" Text="{Binding Path}" Watermark="Download Path"/>
<Button Grid.Column="2" Command="{Binding BrowseCommand}">Browse</Button>
</Grid>
<CheckBox IsChecked="{Binding CacheResources}">Cache Resources</CheckBox>
<CheckBox IsChecked="{Binding Log}">Enable Logs</CheckBox>
<Grid ColumnDefinitions="*,Auto">
<Button Grid.Column="1" Command="{Binding SaveCommand}">Save</Button>
</Grid>
</StackPanel>
</UserControl>

View File

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

View File

@ -0,0 +1,41 @@
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

@ -1,15 +1,222 @@
using Tesses.CMS.Client; using Tesses.CMS.Client;
using CommandLine;
using System.Net;
using Newtonsoft.Json;
TessesCMSClient client = new TessesCMSClient("http://192.168.0.158:62444/"); var prefs=Prefs.Create(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),"tcms.json"));
await foreach(var item in client.Movies.GetMoviesAsync("tesses"))
var res =Parser.Default.ParseArguments<MoviesOptions,ShowsOptions,UsersOptions,EventsOptions,EndpointOptions>(args);
res = await res.WithParsedAsync<MoviesOptions>(MoviesCallback);
res = await res.WithParsedAsync<ShowsOptions>(ShowsCallback);
res = await res.WithParsedAsync<UsersOptions>(UsersCallback);
res = await res.WithParsedAsync<EndpointOptions>(EndpointCallback);
res = res.WithParsed<EventsOptions>(EventsCallback);
void EventsCallback(EventsOptions options)
{ {
var res=await client.Movies.GetMovieContentMetadataAsync("tesses",item.Name); Console.WriteLine("About to read events");
Console.WriteLine(item.ProperName); using(var cms = new TessesCMSClient(prefs.Url))
Console.WriteLine($"\t{res.PosterUrl}"); {
using(CancellationTokenSource src=new CancellationTokenSource()){
cms.StartEvents((evt)=>{
Console.WriteLine($"Type: {evt.Type}");
Console.WriteLine($"Username: {evt.Username}");
Console.WriteLine($"Userpropername: {evt.UserProperName}");
Console.WriteLine($"Name: {evt.Name}");
Console.WriteLine($"ProperName: {evt.ProperName}");
bool hasBody = !string.IsNullOrWhiteSpace(evt.Body);
if(!string.IsNullOrWhiteSpace(evt.Description))
{
Console.WriteLine("Description:");
Console.WriteLine(evt.Description);
if(hasBody)
Console.WriteLine();
}
if(hasBody)
{
Console.WriteLine("Body:");
Console.WriteLine(evt.Body);
}
Console.WriteLine();
},src.Token);
Console.ReadLine();
src.Cancel();
}
}
} }
//await client.Movies.CreateAsync("HolyLoop","Holy Loop");
await client.Users.LogoutAsync(); async Task ShowsCallback(ShowsOptions options)
{
throw new NotImplementedException();
}
async Task UsersCallback(UsersOptions options)
{
if(options.Login)
{
Console.Write("Email: ");
string? email=Console.ReadLine();
if(string.IsNullOrWhiteSpace(email))
{
Console.WriteLine("Email is empty");
return;
}
string password = ReadLine.ReadPassword($"Password for {email}: ");
using(var clt = new TessesCMSClient(prefs.Url))
{
var cookie = await clt.Users.GetCookieAsync(email,password);
if(cookie.Success)
{
if(!string.IsNullOrWhiteSpace(prefs.Session))
{
await clt.Users.SetCookieAsync(prefs.Session);
await clt.Users.LogoutAsync();
}
prefs.Session = cookie.Cookie;
prefs.Save();
}
}
}
else if(options.Logout)
{
if(string.IsNullOrWhiteSpace(prefs.Session))
{
Console.WriteLine("Not logged in");
return;
}
using(var clt = new TessesCMSClient(prefs.Url))
{
await clt.Users.SetCookieAsync(prefs.Session);
await clt.Users.LogoutAsync();
prefs.Session="";
prefs.Save();
}
Console.WriteLine("Logged out");
}
else {
using(var clt = new TessesCMSClient(prefs.Url))
{
await foreach(var user in clt.Users.GetUsersAsync())
{
Console.WriteLine($"Username: {user.Username}");
Console.WriteLine($"Name: {user.ProperName}");
Console.WriteLine("About:");
Console.WriteLine(user.AboutMe);
Console.WriteLine();
}
}
}
}
async Task EndpointCallback(EndpointOptions options)
{
if(string.IsNullOrWhiteSpace(options.Url))
{
Console.WriteLine($"Current Endpoint: {prefs.Url}");
}
else
{
prefs.Url = options.Url;
prefs.Save();
Console.WriteLine($"Set Current Endpoint To: {options.Url}");
}
}
async Task MoviesCallback(MoviesOptions options)
{
if(options.List)
{
using(var clt = new TessesCMSClient(prefs.Url))
{
await foreach(var movie in clt.Movies.GetMoviesAsync(options.Username))
{
Console.WriteLine($"Title (ProperName): {movie.ProperName}");
Console.WriteLine($"Name: {movie.Name}");
Console.WriteLine($"Created: {movie.CreationTime}");
Console.WriteLine($"Last Updated: {movie.LastUpdated}");
Console.WriteLine("Description: ");
Console.WriteLine(movie.Description);
Console.WriteLine();
}
}
}
}
[Verb("endpoint",false,new string[]{"ep"},HelpText ="Change endpoint")]
internal class EndpointOptions
{
[Value(0,Required =false,HelpText = "Main Page URL for placing request")]
public string Url {get;set;} = "";
}
[Verb("users",false,HelpText = "User accounts")]
internal class UsersOptions
{
[Option('l',"login",Required =false,HelpText = "Login")]
public bool Login {get;set;}=false;
[Option('o',"logout",Required = false,HelpText = "Logout")]
public bool Logout {get;set;}=false;
}
internal class ShowsOptions
{
}
[Verb("movies",false,HelpText ="The movies")]
internal class MoviesOptions
{
[Option('l',"list",HelpText = "List movies")]
public bool List {get;set;}=false;
[Value(0,Required =true,HelpText = "Username")]
public string Username {get;set;}="";
}
internal class Prefs
{
[JsonProperty("url")]
public string Url {get;set;}="https://tessesstudios.com/";
[JsonProperty("session")]
public string Session {get;set;}="";
[JsonIgnore]
string filename{get;set;}="";
public static Prefs Create(string path)
{
if(File.Exists(path))
{
var res=JsonConvert.DeserializeObject<Prefs>(File.ReadAllText(path)) ?? new Prefs();
res.filename = path;
return res;
}
else
{
return new Prefs(){filename=path};
}
}
public void Save()
{
File.WriteAllText(filename,JsonConvert.SerializeObject(this));
}
}
[Verb("events",false,new string[]{"evts"},HelpText ="Read server sent events (for debugging)")]
internal class EventsOptions
{
}

View File

@ -4,6 +4,11 @@
<ProjectReference Include="..\Tesses.CMS.Client\Tesses.CMS.Client.csproj" /> <ProjectReference Include="..\Tesses.CMS.Client\Tesses.CMS.Client.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="ReadLine" Version="2.0.1" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>

View File

@ -2,17 +2,36 @@
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
namespace Tesses.CMS.Client namespace Tesses.CMS.Client
{ {
public class TessesCMSClient public enum TessesCMSContentType
{ {
Movie,
Show,
Album,
MusicVideo,
SoftwareProject,
Other
}
public class TessesCMSClient : IDisposable
{
private static HttpClient CreateHttpClient() private static HttpClient CreateHttpClient()
{ {
HttpClientHandler httpClientHandler=new HttpClientHandler(); HttpClientHandler httpClientHandler=new HttpClientHandler();
@ -24,28 +43,88 @@ namespace Tesses.CMS.Client
internal HttpClient client; internal HttpClient client;
internal string rooturl; internal string rooturl;
public HttpClient Client => client; public HttpClient Client => client;
public string RootUrl => $"{rooturl}/"; public string RootUrl
public TessesCMSClient(string url,HttpClient client)
{ {
get=>$"{rooturl}/";
set {
rooturl = value.TrimEnd('/');
}
}
bool ownsClient;
public TessesCMSClient(string url,HttpClient client,bool ownsClient)
{
this.ownsClient=ownsClient;
this.client = client; this.client = client;
rooturl = url.TrimEnd('/'); rooturl = url.TrimEnd('/');
} }
public TessesCMSClient(HttpClient client) : this("https://tessesstudios.com/",client) public TessesCMSClient(HttpClient client,bool ownsClient) : this("https://tessesstudios.com/",client,ownsClient)
{ {
} }
public TessesCMSClient(string url) : this(url,CreateHttpClient())
public TessesCMSClient(string url) : this(url,CreateHttpClient(),true)
{ {
} }
public TessesCMSClient() : this(CreateHttpClient()) public TessesCMSClient() : this(CreateHttpClient(),true)
{ {
}
public async Task<Branding> GetBrandingAsync()
{
return JsonConvert.DeserializeObject<Branding>(await client.GetStringAsync($"{rooturl}/api/v1/Branding"));
}
public void StartEvents(Action<CMSEvent> evt,CancellationToken token=default)
{
Task.Run(async()=>{
var clt=await client.GetSSEClientAsync($"{rooturl}/api/v1/Updates");
await foreach(var item in clt.ReadEventsAsync(token))
{
evt(item.ParseJson<CMSEvent>());
}
}).Wait(0);
}
public async Task<bool> CreateAsync(string urlname, string propername, string description, TessesCMSContentType type=TessesCMSContentType.Movie,CancellationToken token=default)
{
Dictionary<string,string> kvp= new Dictionary<string, string>
{
{ "name", urlname },
{ "proper_name", propername },
{ "description", description },
{ "type", type.ToString().ToLower() }
};
using(var res=await client.PostAsync($"{rooturl}/upload",new FormUrlEncodedContent(kvp),token))
{
return res.StatusCode != System.Net.HttpStatusCode.Unauthorized;
}
}
public async Task<bool> UploadFilePutAsync(string url,string file,CancellationToken token=default,IProgress<double> progress=null)
{
using(var f = File.OpenRead(file))
return await UploadFilePutAsync(url,f,token,progress);
}
public async Task<bool> UploadFilePutAsync(string url, Stream src,CancellationToken token=default,IProgress<double> progress=null)
{
var request = new HttpRequestMessage(HttpMethod.Put,url);
request.Content = new ProgressContent(src,progress);
using(var res=await client.SendAsync(request,token))
{
return res.StatusCode != System.Net.HttpStatusCode.Unauthorized;
}
} }
public async Task DownloadFileAsync(string url,string dest,CancellationToken token=default,IProgress<double> progress=null) public async Task DownloadFileAsync(string url,string dest,CancellationToken token=default,IProgress<double> progress=null)
{ {
using(var f = File.Create(dest)) using(var f = File.Open(dest,FileMode.OpenOrCreate,FileAccess.Write))
await DownloadFileAsync(url,f,token,progress); await DownloadFileAsync(url,f,token,progress);
} }
public async Task DownloadFileAsync(string url,Stream dest,CancellationToken token=default,IProgress<double> progress=null) public async Task DownloadFileAsync(string url,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
@ -83,11 +162,88 @@ namespace Tesses.CMS.Client
} while(read>0); } while(read>0);
resp.Dispose(); resp.Dispose();
} }
public ShowClient Shows => new ShowClient(this);
public MovieClient Movies => new MovieClient(this); public MovieClient Movies => new MovieClient(this);
public UserClient Users => new UserClient(this); public UserClient Users => new UserClient(this);
public void Dispose()
{
if(this.ownsClient)
this.client.Dispose();
}
} }
public class Branding
{
[JsonProperty("title")]
public string Title {get;set;}="";
}
[JsonConverter(typeof(StringEnumConverter),typeof(SnakeCaseNamingStrategy))]
public enum EventType
{
MovieCreate,
MovieUpdate,
ShowCreate,
ShowUpdate
}
public class CMSEvent
{
[JsonProperty("eventtype")]
public EventType Type {get;set;}
[JsonProperty("username")]
public string Username {get;set;}="";
[JsonProperty("userpropername")]
public string UserProperName {get;set;}="";
[JsonProperty("name")]
public string Name {get;set;}="";
[JsonProperty("propername")]
public string ProperName {get;set;}="";
[JsonProperty("description")]
public string Description {get;set;}="";
[JsonProperty("body")]
public string Body {get;set;}="";
}
internal class ProgressContent : HttpContent
{
private Stream src;
private IProgress<double> progress;
public ProgressContent(Stream src, IProgress<double> progress)
{
this.src = src;
this.progress = progress;
}
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
int read = 0;
byte[] buffer=new byte[1024];
double offset=0;
double length = src.Length;
do {
read = await src.ReadAsync(buffer,0,buffer.Length);
offset += read;
await stream.WriteAsync(buffer,0,read);
if(length != 0)
progress?.Report(offset / length);
} while(read != 0);
}
protected override bool TryComputeLength(out long length)
{
length = src.Length;
return true;
}
}
public class UserClient public class UserClient
{ {
TessesCMSClient client; TessesCMSClient client;
@ -95,21 +251,115 @@ namespace Tesses.CMS.Client
{ {
this.client = client; this.client = client;
} }
public async Task LogoutAsync() public async Task LogoutAsync()
{ {
await client.client.GetStringAsync($"{client.rooturl}/logout"); (await client.client.GetAsync("/logout")).Dispose();
} }
public async Task<bool> LoginAsync(string email,string password)
public async Task<LoginToken> CreateTokenAsync(string email,string password)
{ {
Dictionary<string,string> us=new Dictionary<string, string>(); Dictionary<string,string> us=new Dictionary<string, string>();
us.Add("email",email); us.Add("email",email);
us.Add("password",password); us.Add("password",password);
using(var res=await client.client.PostAsync($"{client.rooturl}/login", new FormUrlEncodedContent(us))) us.Add("type","json");
return res.StatusCode != System.Net.HttpStatusCode.Unauthorized; using(var res=await client.client.PostAsync($"{client.rooturl}/api/v1/Login", new FormUrlEncodedContent(us)))
return JsonConvert.DeserializeObject<LoginToken>( await res.Content.ReadAsStringAsync());
}
public string LoginToken
{
get {
if(client.client.DefaultRequestHeaders.Contains("Authorization"))
return client.client.DefaultRequestHeaders.Authorization.Parameter;
return "";
}
set{
if(string.IsNullOrWhiteSpace(value))
if(client.client.DefaultRequestHeaders.Contains("Authorization"))
client.client.DefaultRequestHeaders.Remove("Authorization");
else
client.client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",value);
}
}
public async IAsyncEnumerable<UserAccount> GetUsersAsync()
{
foreach(var user in JsonConvert.DeserializeObject<List<UserAccount>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetPublicUsers")))
{
yield return user;
}
} }
} }
public class MovieClient public class LoginToken
{
[JsonProperty("success")]
public bool Success {get;set;}=false;
[JsonProperty("cookie")]
public string Cookie {get;set;}="";
}
public class ShowClient
{
TessesCMSClient client;
internal ShowClient(TessesCMSClient client)
{
this.client = client;
}
public async IAsyncEnumerable<Show> GetShowsAsync(string user)
{
foreach(var item in JsonConvert.DeserializeObject<List<Show>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetShows?user={HttpUtility.UrlEncode(user)}")))
{
yield return item;
}
}
public async IAsyncEnumerable<Season> GetSeasonsAsync(string user,string show)
{
foreach(var item in JsonConvert.DeserializeObject<List<Season>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetSeasons?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(show)}")))
{
yield return item;
}
}
public async IAsyncEnumerable<Episode> GetEpisodesAsync(string user,string show,int season)
{
foreach(var item in JsonConvert.DeserializeObject<List<Episode>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetEpisodes?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(show)}&season={season}")))
{
yield return item;
}
}
public async IAsyncEnumerable<SeasonWithEpisodes> GetEpisodesAsync(string user,string show)
{
await foreach(var season in GetSeasonsAsync(user,show))
{
List<Episode> episodes=new List<Episode>();
await foreach(var episode in GetEpisodesAsync(user,show,season.SeasonNumber))
{
episodes.Add(episode);
}
yield return new SeasonWithEpisodes(season,episodes);
}
}
public async IAsyncEnumerable<ShowWithSeasonsAndEpisodes> GetEpisodesAsync(string user)
{
await foreach(var show in GetShowsAsync(user))
{
List<SeasonWithEpisodes> seasons=new List<SeasonWithEpisodes>();
await foreach(var episode in GetEpisodesAsync(user,show.Name))
{
seasons.Add(episode);
}
yield return new ShowWithSeasonsAndEpisodes(show,seasons);
}
}
}
public class MovieClient
{ {
TessesCMSClient client; TessesCMSClient client;
internal MovieClient(TessesCMSClient client) internal MovieClient(TessesCMSClient client)
@ -118,7 +368,7 @@ namespace Tesses.CMS.Client
} }
public async IAsyncEnumerable<Movie> GetMoviesAsync(string user) public async IAsyncEnumerable<Movie> GetMoviesAsync(string user)
{ {
foreach(var item in JsonConvert.DeserializeObject<List<Movie>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetMovies?user={HttpUtility.UrlDecode(user)}"))) foreach(var item in JsonConvert.DeserializeObject<List<Movie>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetMovies?user={HttpUtility.UrlEncode(user)}")))
{ {
yield return item; yield return item;
} }
@ -127,14 +377,213 @@ namespace Tesses.CMS.Client
{ {
return JsonConvert.DeserializeObject<MovieContentMetaData>(await client.client.GetStringAsync($"{client.rooturl.TrimEnd('/')}/api/v1/MovieFile?movie={movie}&user={user}&type=json")); return JsonConvert.DeserializeObject<MovieContentMetaData>(await client.client.GetStringAsync($"{client.rooturl.TrimEnd('/')}/api/v1/MovieFile?movie={movie}&user={user}&type=json"));
} }
public async Task UploadExtrasAsync(string user, string movie, string dir, CancellationToken token=default,IProgress<double> progress=null)
{
List<(string localPath,string removePath)> items=new List<(string localPath, string removePath)>();
async Task EnumerateDir(string local, string remote)
{
if(!string.IsNullOrWhiteSpace(remote))
await CreateExtraDirectoryAsync(user,movie,remote,token);
foreach(var dir in Directory.EnumerateDirectories(local))
{
await EnumerateDir(dir,$"{remote}/{Path.GetFileName(dir)}");
}
foreach(var file in Directory.EnumerateFiles(local))
{
string name = $"{remote}/{Path.GetFileName(file)}";
items.Add((file,name));
}
}
await EnumerateDir(dir,"");
for(int i = 0;i<items.Count;i++)
{
await UploadExtraAsync(user,movie,items[i].removePath,items[i].localPath,token,new Progress<double>(e=>{
double j = i + e;
progress?.Report(j / items.Count);
}));
}
}
public async Task<bool> UploadExtraAsync(string user, string movie,string extra, string src,CancellationToken token=default,IProgress<double> progress=null)
{
string url = $"{client.rooturl}/api/v1/MovieExtra?user={user}&movie={movie}&extra={HttpUtility.UrlEncode(extra.TrimStart('/'))}";
return await client.UploadFilePutAsync(url,src,token,progress);
}
public async Task<bool> UploadExtraAsync(string user, string movie,string extra, Stream src,CancellationToken token=default,IProgress<double> progress=null)
{
string url = $"{client.rooturl}/api/v1/MovieExtra?user={user}&movie={movie}&extra={HttpUtility.UrlEncode(extra.TrimStart('/'))}";
return await client.UploadFilePutAsync(url,src,token,progress);
}
public async Task<bool> UploadMovieAsync(string user, string movie, string src,CancellationToken token=default,IProgress<double> progress=null)
{
string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=movie";
return await client.UploadFilePutAsync(url,src,token,progress);
}
public async Task<bool> UploadMovieAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress<double> progress=null)
{
string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=movie";
return await client.UploadFilePutAsync(url,src,token,progress);
}
public async Task<bool> UploadPosterAsync(string user, string movie, string src,CancellationToken token=default,IProgress<double> progress=null)
{
string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=poster";
return await client.UploadFilePutAsync(url,src,token,progress);
}
public async Task<bool> UploadPosterAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress<double> progress=null)
{
string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=poster";
return await client.UploadFilePutAsync(url,src,token,progress);
}
public async Task<bool> UploadThumbnailAsync(string user, string movie, string src,CancellationToken token=default,IProgress<double> progress=null)
{
string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=thumbnail";
return await client.UploadFilePutAsync(url,src,token,progress);
}
public async Task<bool> UploadThumbnailAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress<double> progress=null)
{
string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=thumbnail";
return await client.UploadFilePutAsync(url,src,token,progress);
}
public async Task<bool> CreateExtraDirectoryAsync(string user,string movie, string extraDir,CancellationToken token=default)
{
string parent="";
string name=Path.GetFileName(extraDir);
try{
parent=Path.GetDirectoryName(extraDir.TrimStart('/'));
}
catch(Exception)
{
parent="";
}
Dictionary<string,string> kvp= new Dictionary<string, string>
{
{ "name", name },
{ "parent", parent },
};
using(var res=await client.client.PostAsync($"{client.rooturl}/user/{user}/movie/{movie}/mkdir",new FormUrlEncodedContent(kvp),token))
{
return res.StatusCode != System.Net.HttpStatusCode.Unauthorized;
}
}
public async Task DownloadMovieAsync(string user,string movie,string dest,CancellationToken token=default,IProgress<double> progress=null) public async Task DownloadMovieAsync(string user,string movie,string dest,CancellationToken token=default,IProgress<double> progress=null)
{ {
await client.DownloadFileAsync($"{client.rooturl.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress); await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress);
} }
public async Task DownloadMovieAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress<double> progress=null) public async Task DownloadMovieAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{ {
await client.DownloadFileAsync($"{client.rooturl.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress); await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress);
}
public async Task DownloadThumbnailAsync(string user,string movie,string dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/thumbnail.jpg",dest,token,progress);
}
public async Task DownloadThumbnailAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/thumbnail.jpg",dest,token,progress);
}
public async Task DownloadPosterAsync(string user,string movie,string dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/poster.jpg",dest,token,progress);
}
public async Task DownloadPosterAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/poster.jpg",dest,token,progress);
}
public async Task DownloadExtraAsync(string user,string movie,string path,string dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/extras/{HttpUtility.UrlPathEncode(path.TrimStart('/'))}",dest,token,progress);
}
public async Task DownloadExtraAsync(string user,string movie,string path,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/extras/{HttpUtility.UrlPathEncode(path.TrimStart('/'))}",dest,token,progress);
}
public async Task DownloadTorrentAsync(string user,string movie,string dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}.torrent",dest,token,progress);
}
public async Task DownloadTorrentAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}.torrent",dest,token,progress);
}
public async Task DownloadTorrentWithExtrasAsync(string user,string movie,string dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}_withextras.torrent",dest,token,progress);
}
public async Task DownloadTorrentWithExtrasAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}_withextras.torrent",dest,token,progress);
}
public async Task DownloadExtrasAsync(string user,string movie, string dir, CancellationToken token=default,IProgress<double> progress=null)
{
var r = await GetMovieContentMetadataAsync(user,movie);
List<(string inDir,string outDir)> items=new List<(string inDir, string outDir)>();
void DownloadExtraDir(List<ExtraDataStream> files,string outDir,string inDir)
{
Directory.CreateDirectory(outDir);
foreach(var item in files)
{
if(item.IsDir)
{
DownloadExtraDir(item.Items,Path.Combine(outDir,item.Name),$"{inDir}/{item.Name}");
}
else
{
items.Add(($"{inDir}/{item.Name}",Path.Combine(outDir,item.Name)));
}
}
}
DownloadExtraDir(r.ExtraStreams,dir,"");
for(int i = 0;i<items.Count;i++)
{
await DownloadExtraAsync(user,movie,items[i].inDir,items[i].outDir,token,new Progress<double>(e=>{
double j = i + e;
progress?.Report(j / items.Count);
}));
}
}
public async Task<bool> CreateAsync(string urlname, string propername, string description)
{
return await client.CreateAsync(urlname,propername,description);
} }
} }
} }

View File

@ -0,0 +1,25 @@
using System;
using Newtonsoft.Json;
namespace Tesses.CMS.Client
{
public class Episode
{
[JsonProperty("proper_name")]
public string ProperName {get;set;}="";
[JsonProperty("name")]
public string Name {get;set;}="";
[JsonProperty("season_number")]
public int SeasonNumber {get;set;}
[JsonProperty("episode_number")]
public int EpisodeNumber {get;set;}
[JsonProperty("episode_name")]
public string EpisodeName {get;set;}="";
[JsonProperty("creation_time")]
public DateTime CreationTime {get;set;}
[JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;}
[JsonProperty("description")]
public string Description {get;set;}="";
}
}

View File

@ -6,21 +6,34 @@ namespace Tesses.CMS.Client
{ {
public class MovieContentMetaData public class MovieContentMetaData
{ {
[JsonProperty("has_movie_torrent")]
public bool HasMovieTorrent{get;set;}
[JsonProperty("movie_torrent_url")] [JsonProperty("movie_torrent_url")]
public string MovieTorrentUrl {get;set;} public string MovieTorrentUrl {get;set;}
[JsonProperty("has_movie_with_extras_torrent")]
public bool HasMovieWithExtrasTorrent{get;set;}
[JsonProperty("movie_with_extras_torrent_url")] [JsonProperty("movie_with_extras_torrent_url")]
public string MovieWithExtrasTorrentUrl {get;set;} public string MovieWithExtrasTorrentUrl {get;set;}
[JsonProperty("has_browser_stream")]
public bool HasBrowserStream {get;set;}
[JsonProperty("has_download_stream")]
public bool HasDownloadStream {get;set;}
[JsonProperty("browser_stream")] [JsonProperty("browser_stream")]
public string BrowserStream {get;set;} public string BrowserStream {get;set;}
[JsonProperty("download_stream")] [JsonProperty("download_stream")]
public string DownloadStream {get;set;} public string DownloadStream {get;set;}
[JsonProperty("has_poster")]
public bool HasPoster {get;set;}
[JsonProperty("poster_url")] [JsonProperty("poster_url")]
public string PosterUrl {get;set;} public string PosterUrl {get;set;}
[JsonProperty("has_thumbnail")]
public bool HasThumbnail {get;set;}
[JsonProperty("thumbnail_url")] [JsonProperty("thumbnail_url")]
public string ThumbnailUrl {get;set;} public string ThumbnailUrl {get;set;}
@ -56,14 +69,15 @@ namespace Tesses.CMS.Client
public class Movie public class Movie
{ {
[JsonProperty("proper_name")]
public string ProperName {get;set;} public string ProperName {get;set;}
[JsonProperty("name")]
public string Name {get;set;} public string Name {get;set;}
[JsonProperty("creation_time")]
public DateTime CreationTime {get;set;} public DateTime CreationTime {get;set;}
[JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;} public DateTime LastUpdated {get;set;}
[JsonProperty("description")]
public string Description {get;set;} public string Description {get;set;}
} }
} }

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Tesses.CMS.Client
{
public static class SSEExtensions
{
public static async Task<SSEClient> GetSSEClientAsync(this HttpClient clt,string url)
{
var strm=await clt.GetStreamAsync(url);
return new SSEClient(strm);
}
}
public class SSEEvent
{
public SSEEvent(string data)
{
Data=data;
}
public string Data {get;set;}
public T ParseJson<T>()
{
return JsonConvert.DeserializeObject<T>(Data);
}
}
public class SSEClient
{
Stream strm;
public SSEClient(Stream strm)
{
this.strm=strm;
}
public async IAsyncEnumerable<SSEEvent> ReadEventsAsync([EnumeratorCancellation]CancellationToken token=default(CancellationToken))
{
using(var sr = new StreamReader(strm)){
token.Register(()=>{
sr.Dispose();
});
while(!token.IsCancellationRequested)
{
string text="";
try{
text=await sr.ReadLineAsync();
}catch(Exception ex)
{
_=ex;
}
if(!string.IsNullOrWhiteSpace(text))
yield return new SSEEvent(text.Substring(6));
}
}
}
}
}

View File

@ -0,0 +1,22 @@
using System;
using Newtonsoft.Json;
namespace Tesses.CMS.Client
{
public class Season
{
[JsonProperty("proper_name")]
public string ProperName {get;set;}="";
[JsonProperty("name")]
public string Name {get;set;}="";
[JsonProperty("season_number")]
public int SeasonNumber {get;set;}
[JsonProperty("creation_time")]
public DateTime CreationTime {get;set;}
[JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;}
[JsonProperty("description")]
public string Description {get;set;}="";
}
}

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
namespace Tesses.CMS.Client
{
public class SeasonWithEpisodes
{
public SeasonWithEpisodes(Season season,List<Episode> episodes)
{
ProperName = season.ProperName;
Name = season.Name;
SeasonNumber = season.SeasonNumber;
CreationTime = season.CreationTime;
LastUpdated = season.LastUpdated;
Description = season.Description;
Episodes = episodes;
}
public string ProperName {get;set;}="";
public string Name {get;set;}="";
public int SeasonNumber {get;set;}
public DateTime CreationTime {get;set;}
public DateTime LastUpdated {get;set;}
public string Description {get;set;}="";
public List<Episode> Episodes {get;set;}
}
}

20
Tesses.CMS.Client/Show.cs Normal file
View File

@ -0,0 +1,20 @@
using System;
using Newtonsoft.Json;
namespace Tesses.CMS.Client
{
public class Show
{
[JsonProperty("proper_name")]
public string ProperName {get;set;}="";
[JsonProperty("name")]
public string Name {get;set;}="";
[JsonProperty("creation_time")]
public DateTime CreationTime {get;set;}
[JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;}
[JsonProperty("description")]
public string Description {get;set;}="";
}
}

View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace Tesses.CMS.Client
{
public class ShowWithSeasonsAndEpisodes
{
public ShowWithSeasonsAndEpisodes(Show show,List<SeasonWithEpisodes> seasons)
{
ProperName = show.ProperName;
Name = show.Name;
CreationTime = show.CreationTime;
LastUpdated = show.LastUpdated;
Description = show.Description;
Seasons = seasons;
}
public string ProperName {get;set;}="";
public string Name {get;set;}="";
public DateTime CreationTime {get;set;}
public DateTime LastUpdated {get;set;}
public string Description {get;set;}="";
public List<SeasonWithEpisodes> Seasons {get;set;}
}
}

View File

@ -0,0 +1,14 @@
using Newtonsoft.Json;
namespace Tesses.CMS.Client
{
public class UserAccount
{
[JsonProperty("username")]
public string Username {get;set;}="";
[JsonProperty("proper_name")]
public string ProperName {get;set;}="";
[JsonProperty("about_me")]
public string AboutMe {get;set;}="";
}
}

View File

@ -46,6 +46,11 @@ namespace Tesses.CMS.Providers
return false; return false;
} }
public Episode CreateEpisode(string user, string show, int season, int episode, string episodename, string properName, string description)
{
throw new NotImplementedException();
}
public Movie CreateMovie(string user, string movie, string properName, string description) public Movie CreateMovie(string user, string movie, string properName, string description)
{ {
var userId=GetUserAccount(user).Id; var userId=GetUserAccount(user).Id;
@ -59,11 +64,21 @@ namespace Tesses.CMS.Providers
return GetMovie(user,movie); return GetMovie(user,movie);
} }
public Season CreateSeason(string user, string show, int season, string properName, string description)
{
throw new NotImplementedException();
}
public void CreateSession(string session, long account) public void CreateSession(string session, long account)
{ {
con.Execute("INSERT INTO Sessions(Session,Account) values (@session,@account);",new{session,account}); con.Execute("INSERT INTO Sessions(Session,Account) values (@session,@account);",new{session,account});
} }
public Show CreateShow(string user, string show, string properName, string description)
{
throw new NotImplementedException();
}
public void CreateUser(CMSConfiguration configuration, string user, string properName, string email, string password) public void CreateUser(CMSConfiguration configuration, string user, string properName, string email, string password)
{ {
bool first=con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users LIMIT 0, 1;") == null; bool first=con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users LIMIT 0, 1;") == null;
@ -99,6 +114,16 @@ namespace Tesses.CMS.Providers
con.Execute("DELETE FROM VerificationCodes WHERE Session = @code;",new{code}); con.Execute("DELETE FROM VerificationCodes WHERE Session = @code;",new{code});
} }
public int EpisodeCount(string user, string show, int season)
{
throw new NotImplementedException();
}
public Episode GetEpisode(string user, string show, int season, int episode)
{
throw new NotImplementedException();
}
public UserAccount GetFirstUser() public UserAccount GetFirstUser()
{ {
return con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users LIMIT 0, 1;")?.Account; return con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users LIMIT 0, 1;")?.Account;
@ -131,11 +156,26 @@ namespace Tesses.CMS.Providers
} }
} }
public Season GetSeason(string user, string show, int season)
{
throw new NotImplementedException();
}
public long? GetSession(string session) public long? GetSession(string session)
{ {
return con.QueryFirstOrDefault("SELECT * FROM Sessions WHERE Session = @session;",new{session})?.Id; return con.QueryFirstOrDefault("SELECT * FROM Sessions WHERE Session = @session;",new{session})?.Id;
} }
public Show GetShow(string user, string show)
{
throw new NotImplementedException();
}
public IEnumerable<Show> GetShows(string user)
{
throw new NotImplementedException();
}
public UserAccount GetUserAccount(string user) public UserAccount GetUserAccount(string user)
{ {
return con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users WHERE Username=@user;",new{user})?.Account; return con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users WHERE Username=@user;",new{user})?.Account;
@ -170,12 +210,32 @@ namespace Tesses.CMS.Providers
return null; return null;
} }
public int SeasonCount(string user, string show)
{
throw new NotImplementedException();
}
public void UpdateEpisode(Episode episode)
{
throw new NotImplementedException();
}
public void UpdateMovie(Movie movie) public void UpdateMovie(Movie movie)
{ {
DapperMovie dapperMovie=new DapperMovie(movie); DapperMovie dapperMovie=new DapperMovie(movie);
con.Execute("UPDATE Users set movie = @movie WHERE Id = @id;",new{movie=dapperMovie,id=movie.Id}); con.Execute("UPDATE Users set movie = @movie WHERE Id = @id;",new{movie=dapperMovie,id=movie.Id});
} }
public void UpdateSeason(Season season)
{
throw new NotImplementedException();
}
public void UpdateShow(Show show)
{
throw new NotImplementedException();
}
public void UpdateUser(UserAccount account) public void UpdateUser(UserAccount account)
{ {
DapperUserAccount account1=new DapperUserAccount(account); DapperUserAccount account1=new DapperUserAccount(account);

View File

@ -18,17 +18,13 @@ public class LiteDBContentProvider : IContentProvider
{ {
var userId=GetUserAccount(user).Id; var userId=GetUserAccount(user).Id;
Movie _movie = new Movie(){UserId = userId,Name = movie,ProperName=properName,Description = description}; return CreateMovie(userId,movie,properName,description);
_movie.Id=Movies.Insert(_movie);
return _movie;
} }
public Show CreateShow(string user, string show, string properName, string description) public Show CreateShow(string user, string show, string properName, string description)
{ {
var userId=GetUserAccount(user).Id; var userId=GetUserAccount(user).Id;
Show _show = new Show(){UserId = userId,Name = show,ProperName=properName,Description = description}; return CreateShow(userId,show,properName,description);
_show.Id=Shows.Insert(_show);
return _show;
} }
private ILiteCollection<UserAccount> UserAccounts => db.GetCollection<UserAccount>("users"); private ILiteCollection<UserAccount> UserAccounts => db.GetCollection<UserAccount>("users");
private ILiteCollection<Movie> Movies => db.GetCollection<Movie>("movies"); private ILiteCollection<Movie> Movies => db.GetCollection<Movie>("movies");
@ -39,7 +35,7 @@ public class LiteDBContentProvider : IContentProvider
private ILiteCollection<LiteDbSession> Sessions => db.GetCollection<LiteDbSession>("sessions"); private ILiteCollection<LiteDbSession> Sessions => db.GetCollection<LiteDbSession>("sessions");
private ILiteCollection<LiteDbVerificationToken> VerificationCodes => db.GetCollection<LiteDbVerificationToken>("verificationcodes"); private ILiteCollection<LiteDbVerificationToken> VerificationCodes => db.GetCollection<LiteDbVerificationToken>("verificationcodes");
private ILiteCollection<Album> Albums => db.GetCollection<Album>("albums");
public UserAccount GetFirstUser() public UserAccount GetFirstUser()
{ {
return GetUsers().First(); return GetUsers().First();
@ -105,10 +101,12 @@ public class LiteDBContentProvider : IContentProvider
public void UpdateMovie(Movie movie) public void UpdateMovie(Movie movie)
{ {
movie.LastUpdated = DateTime.Now;
Movies.Update(movie); Movies.Update(movie);
} }
public void UpdateShow(Show show) public void UpdateShow(Show show)
{ {
show.LastUpdated = DateTime.Now;
Shows.Update(show); Shows.Update(show);
} }
@ -210,13 +208,7 @@ public class LiteDBContentProvider : IContentProvider
var myShow = GetShow(user,show); var myShow = GetShow(user,show);
var userId = myShow.UserId; var userId = myShow.UserId;
var showId = myShow.Id; var showId = myShow.Id;
int seasonLargest=0; return SeasonCount(userId,showId);
foreach(var item in Seasons.Find(e=>e.ShowId==showId && e.UserId == userId))
{
if(item.SeasonNumber > seasonLargest)
seasonLargest = item.SeasonNumber;
}
return seasonLargest;
} }
public Season GetSeason(string user, string show, int season) public Season GetSeason(string user, string show, int season)
@ -224,7 +216,7 @@ public class LiteDBContentProvider : IContentProvider
var myShow = GetShow(user,show); var myShow = GetShow(user,show);
var userId = myShow.UserId; var userId = myShow.UserId;
var showId = myShow.Id; var showId = myShow.Id;
return Seasons.FindOne(e=>e.ShowId == showId && e.UserId == userId && e.SeasonNumber == season); return GetSeason(userId,showId,season);
} }
public Season CreateSeason(string user, string show, int season, string properName, string description) public Season CreateSeason(string user, string show, int season, string properName, string description)
@ -232,9 +224,7 @@ public class LiteDBContentProvider : IContentProvider
var myShow = GetShow(user,show); var myShow = GetShow(user,show);
var userId = myShow.UserId; var userId = myShow.UserId;
var showId = myShow.Id; var showId = myShow.Id;
Season _season = new Season(){UserId = userId,ShowId = showId,ProperName=properName,Description = description, SeasonNumber=season}; return CreateSeason(userId,showId,season,properName,description);
_season.Id=Seasons.Insert(_season);
return _season;
} }
public int EpisodeCount(string user, string show, int season) public int EpisodeCount(string user, string show, int season)
@ -242,22 +232,16 @@ public class LiteDBContentProvider : IContentProvider
var myShow = GetShow(user,show); var myShow = GetShow(user,show);
var userId = myShow.UserId; var userId = myShow.UserId;
var showId = myShow.Id; var showId = myShow.Id;
int episodeLargest=0; return EpisodeCount(userId,showId,season);
foreach(var item in Episodes.Find(e=>e.ShowId==showId && e.UserId == userId && e.SeasonNumber == season))
{
if(item.EpisodeNumber > episodeLargest)
episodeLargest = item.EpisodeNumber;
}
return episodeLargest;
} }
public Episode GetEpisode(string user, string show, int season, int episode) public Episode GetEpisode(string user, string show, int season, int episode)
{ {
var myShow = GetShow(user,show); var myShow = GetShow(user,show);
var userId = myShow.UserId; var userId = myShow.UserId;
var showId = myShow.Id; var showId = myShow.Id;
return Episodes.FindOne(e=>e.ShowId == showId && e.UserId == userId && e.SeasonNumber == season && e.EpisodeNumber == episode); return GetEpisode(userId,showId,season,episode);
} }
public Episode CreateEpisode(string user, string show, int season, int episode, string episodename, string properName, string description) public Episode CreateEpisode(string user, string show, int season, int episode, string episodename, string properName, string description)
@ -265,19 +249,136 @@ public class LiteDBContentProvider : IContentProvider
var myShow = GetShow(user,show); var myShow = GetShow(user,show);
var userId = myShow.UserId; var userId = myShow.UserId;
var showId = myShow.Id; var showId = myShow.Id;
Episode _episode = new Episode(){UserId = userId,ShowId = showId,ProperName=properName,Description = description, SeasonNumber=season, EpisodeNumber = episode,EpisodeName=episodename}; return CreateEpisode(userId,showId,season,episode,episodename,properName,description);
_episode.Id=Episodes.Insert(_episode);
return _episode;
} }
public void UpdateEpisode(Episode episode) public void UpdateEpisode(Episode episode)
{ {
episode.LastUpdated = DateTime.Now;
Episodes.Update(episode); Episodes.Update(episode);
} }
public void UpdateSeason(Season season) public void UpdateSeason(Season season)
{ {
season.LastUpdated = DateTime.Now;
Seasons.Update(season); Seasons.Update(season);
} }
public Album CreateAlbum(string user, string album, string properName, string description)
{
var userId=GetUserAccount(user).Id;
return CreateAlbum(userId,album,properName,description);
}
public Album GetAlbum(string user, string album)
{
var userId=GetUserAccount(user).Id;
return GetAlbum(userId,album);
}
public void UpdateAlbum(Album album)
{
album.LastUpdated = DateTime.Now;
Albums.Update(album);
}
public IEnumerable<Album> GetAlbums(string user)
{
return GetAlbums(GetUserAccount(user).Id);
}
public IEnumerable<Album> GetAlbums(long user)
{
return Albums.Find(e=>e.UserId == user);
}
public Movie CreateMovie(long user, string movie, string properName, string description)
{
Movie _movie = new Movie(){UserId = user,Name = movie,ProperName=properName,Description = description};
_movie.CreationTime = DateTime.Now;
_movie.LastUpdated = DateTime.Now;
_movie.Id=Movies.Insert(_movie);
return _movie;
}
public Album CreateAlbum(long user, string album, string properName, string description)
{
Album _album = new Album(){UserId = user,Name = album,ProperName=properName,Description = description};
_album.CreationTime = DateTime.Now;
_album.LastUpdated = DateTime.Now;
_album.Id=Albums.Insert(_album);
return _album;
}
public Show CreateShow(long user, string show, string properName, string description)
{
Show _show = new Show(){UserId = user,Name = show,ProperName=properName,Description = description};
_show.CreationTime = DateTime.Now;
_show.LastUpdated = DateTime.Now;
_show.Id=Shows.Insert(_show);
return _show;
}
public Show GetShow(long user, long show)
{
return Shows.FindOne(e=>e.Id == show && e.UserId == user);
}
public int SeasonCount(long user, long show)
{
int seasonLargest=0;
foreach(var item in Seasons.Find(e=>e.ShowId==show && e.UserId == user))
{
if(item.SeasonNumber > seasonLargest)
seasonLargest = item.SeasonNumber;
}
return seasonLargest;
}
public Season GetSeason(long user, long show, int season)
{
return Seasons.FindOne(e=>e.ShowId == show && e.UserId == user && e.SeasonNumber == season);
}
public Season CreateSeason(long user, long show, int season, string properName, string description)
{
Season _season = new Season(){UserId = user,ShowId = show,ProperName=properName,Description = description, SeasonNumber=season};
_season.CreationTime = DateTime.Now;
_season.LastUpdated = DateTime.Now;
_season.Id=Seasons.Insert(_season);
return _season;
}
public int EpisodeCount(long user, long show, int season)
{
int episodeLargest=0;
foreach(var item in Episodes.Find(e=>e.ShowId==show && e.UserId == user && e.SeasonNumber == season))
{
if(item.EpisodeNumber > episodeLargest)
episodeLargest = item.EpisodeNumber;
}
return episodeLargest;
}
public Episode GetEpisode(long user, long show, int season, int episode)
{
return Episodes.FindOne(e=>e.ShowId == show && e.UserId == user && e.SeasonNumber == season && e.EpisodeNumber == episode);
}
public Episode CreateEpisode(long user, long show, int season, int episode, string episodename, string properName, string description)
{
Episode _episode = new Episode(){UserId = user,ShowId = show,ProperName=properName,Description = description, SeasonNumber=season, EpisodeNumber = episode,EpisodeName=episodename};
_episode.CreationTime = DateTime.Now;
_episode.LastUpdated = DateTime.Now;
_episode.Id=Episodes.Insert(_episode);
return _episode;
}
public Album GetAlbum(long user, string album)
{
return Albums.FindOne(e=>e.Name == album && e.UserId == user);
}
} }
} }

View File

@ -2,7 +2,7 @@
<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>
</PropertyGroup> </PropertyGroup>

43
Tesses.CMS/Album.cs Normal file
View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Tesses.CMS
{
public class Album
{
[JsonIgnore]
public long Id {get;set;}
[JsonProperty("proper_name")]
public string ProperName {get;set;}
[JsonProperty("name")]
public string Name {get;set;}
[JsonProperty("album_artist")]
public string AlbumArtist {get;set;} = "Unknown Artist";
[JsonProperty("tracks")]
public List<string> Tracks {get;set;}=new List<string>();
[JsonProperty("year")]
public int Year {get;set;}=DateTime.Now.Year;
[JsonIgnore]
public long UserId {get;set;}
[JsonProperty("creation_time")]
public DateTime CreationTime {get;set;}
[JsonProperty("last_updated_time")]
public DateTime LastUpdated {get;set;}
[JsonProperty("description")]
public string Description {get;set;}
public object Scriban(string thumbnail)
{
return new {
Proper = System.Web.HttpUtility.HtmlEncode( ProperName),
Name = System.Web.HttpUtility.HtmlEncode(Name),
Description = System.Web.HttpUtility.HtmlEncode(Description),
Thumbnail = thumbnail
};
}
}
}

View File

@ -25,7 +25,7 @@ namespace Tesses.CMS
public override async Task GetAsync(ServerContext ctx) public override async Task GetAsync(ServerContext ctx)
{ {
try{ try{
await ctx.SendTextAsync(await ReadAllTextAsync(ctx.UrlPath),HeyRed.Mime.MimeTypesMap.GetMimeType(ctx.UrlPath)); await ctx.SendStreamAsync(OpenRead(ctx.UrlPath),HeyRed.Mime.MimeTypesMap.GetMimeType(ctx.UrlPath));
}catch(ArgumentNullException ex) }catch(ArgumentNullException ex)
{ {
_=ex; _=ex;

View File

@ -0,0 +1,50 @@
<br>
<div class="container">
<img src="{{albumthumbnail}}" alt="{{albumproper}}" width="120" height="120">
<h1>{{albumproper}}</h1>
<h2>{{userproper}}</h2>
<hr>
<a class="btn btn-primary" href="./play">Listen Online</a>
{{if editable}}
<a class="btn btn-primary" href="./edit">Edit</a>
{{end}}
{{if extrasexists}}
<a class="btn btn-primary" href="./extras">Extras</a>
{{end}}
{{if torrentexists}}
<a class="btn btn-primary" href="{{torrent}}">Torrent</a>
{{end}}
{{if torrentwextraexists}}
<a class="btn btn-primary" href="{{torrentwextra}}">Torrent With Extras</a>
{{end}}
{{if editable}}
<form action="./sendupdate?csrf={{csrf}}" method="post">
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Message To Subscribers (optional)</label>
<textarea name="body" class="form-control" id="exampleFormControlTextarea1" rows="5"></textarea>
</div>
<input type="submit" value="Send Update" class="btn btn-success">
</form>
{{end}}
<br>
<span>Note to Touchscreen users: Touch and hold seekbar to set position in song</span>
<br>
<div class="accordion" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Description
</button>
</h2>
<br>
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
<div class="accordion-body">
<p>{{albumdescription}}</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
<div class="container-fluid">
<!--thanks to https://stackoverflow.com/a/21678797-->
<style>
ul {
text-decoration: none;
}
li {
display: inline-block;
vertical-align: top;
}
</style>
<ul>
{{for album in albums}}
<li>
<img src="{{album.thumbnail}}" width="120" height="120"><br><a href="../album/{{album.name}}/">{{album.proper}}</a>
</li>
{{ end }}
</ul>
</div>

View File

@ -3,6 +3,5 @@
<ul> <ul>
<li><a href="{{rooturl}}api/v1/">Api</a></li> <li><a href="{{rooturl}}api/v1/">Api</a></li>
<li><a href="{{rooturl}}devcenter/webhooks">Webhooks</a></li>
</ul> </ul>
</div> </div>

View File

@ -0,0 +1,35 @@
<h1>Change album metadata</h1>
<form action="./edit?csrf={{csrf}}" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
</div>
<div class="mb-3">
<label for="album-artist" class="form-label">Album Artist</label>
<input type="text" class="form-control" id="album-artist" name="album_artist" value="{{albumartist}}">
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Description</label>
<textarea name="description" class="form-control" id="exampleFormControlTextarea1" rows="5">{{description}}</textarea>
</div>
<div class="mb-3">
<label for="year" class="form-label">Year</label>
<!--Just in case Judgement doesn't happen until 65535 and this source is still available-->
<input type="number" min="0" max="65535" class="form-control" id="year" name="year" value="{{year}}">
</div>
<input type="submit" value="Change Metadata" class="btn btn-primary">
</form>
<h1>Upload album art (uses JPEG)</h1>
<form action="./upload?csrf={{csrf2}}" method="post" enctype="multipart/form-data">
<div class="mb-3">
<label for="formFileSm" class="form-label">File to upload</label>
<input class="form-control form-control-sm" id="formFileSm" name="file" type="file">
</div>
<input type="submit" value="Submit" class="btn btn-primary">
</form>
<a href="./extras/" target="_blank" class="btn btn-primary">View/Edit extras</a>
<a href="./edit_tracklist" target="_blank" class="btn btn-primary">Tracklist</a>

View File

@ -0,0 +1,28 @@
<h1>Change movie metadata</h1>
<form action="./edit" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Description</label>
<textarea name="description" class="form-control" id="exampleFormControlTextarea1" rows="5">{{description}}</textarea>
</div>
<input type="submit" value="Change Metadata" class="btn btn-primary">
</form>
<h1>Upload episode files</h1>
<form action="./upload" method="post" enctype="multipart/form-data">
<h1>You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)</h1>
<select class="form-select" name="type" aria-label="Select resource">
<option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the episode)</option>
<option value="poster">Poster (same resoultion as episode, what you see on video player before pressing play)</option>
<option value="movie">Episode (should be mp4, this file will be downloaded when someone clicks download and will be converted to a browser version)</option>
</select>
<div class="mb-3">
<label for="formFileSm" class="form-label">File to upload</label>
<input class="form-control form-control-sm" id="formFileSm" name="file" type="file">
</div>
<input type="submit" value="Submit" class="btn btn-primary">
</form>

View File

@ -1,5 +1,5 @@
<h1>Change movie metadata</h1> <h1>Change movie metadata</h1>
<form action="./edit" method="post" enctype="application/x-www-form-urlencoded"> <form action="./edit?csrf={{csrf}}" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3"> <div class="mb-3">
<label for="proper-name" class="form-label">Proper Name</label> <label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}"> <input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
@ -12,7 +12,7 @@
</form> </form>
<h1>Upload movie files</h1> <h1>Upload movie files</h1>
<form action="./upload" method="post" enctype="multipart/form-data"> <form action="./upload?csrf={{csrf2}}" method="post" enctype="multipart/form-data">
<h1>You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)</h1> <h1>You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)</h1>
<select class="form-select" name="type" aria-label="Select resource"> <select class="form-select" name="type" aria-label="Select resource">
<option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the movie)</option> <option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the movie)</option>

View File

@ -41,8 +41,8 @@
<form action="./upload" method="post" enctype="multipart/form-data"> <form action="./upload" method="post" enctype="multipart/form-data">
<h1>You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)</h1> <h1>You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)</h1>
<select class="form-select" name="type" aria-label="Select resource"> <select class="form-select" name="type" aria-label="Select resource">
<option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the show (also used for when episode does not have one))</option> <option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the season (also used for when episode does not have one))</option>
<option value="poster">Poster (same resoultion as show (for when episode does not have one), what you see on video player before pressing play)</option> <option value="poster">Poster (same resoultion as season (for when episode does not have one), what you see on video player before pressing play)</option>
</select> </select>
<div class="mb-3"> <div class="mb-3">
<label for="formFileSm" class="form-label">File to upload</label> <label for="formFileSm" class="form-label">File to upload</label>

View File

@ -0,0 +1,10 @@
<h3>Hi {{propername}}</h3>
<p>The album page for {{albumuserproper}}'s album {{albumproper}} has been {{if updated}} updated, {{else}} created, {{end}} you can view it <a href="{{albumurl}}">here.</a></p>
{{if hasmessage}}
<hr>
<h2>Message from {{albumuserproper}}</h2>
<p>{{message}}</p>
<br>
{{end}}

View File

@ -0,0 +1,9 @@
<h3>Hi {{propername}}</h3>
<p>The movie page for {{movieuserproper}}'s movie {{movieproper}} has been {{if updated}} updated, {{else}} created, {{end}} you can view it <a href="{{movieurl}}">here.</a></p>
{{if hasmessage}}
<hr>
<h2>Message from {{movieuserproper}}</h2>
<p>{{message}}</p>
<br>
{{end}}

View File

@ -0,0 +1,10 @@
<h3>Hi {{propername}}</h3>
<p>The show page for {{showuserproper}}'s show {{showproper}} has been {{if updated}} updated, {{else}} created, {{end}} you can view it <a href="{{showurl}}">here.</a></p>
{{if hasmessage}}
<hr>
<h2>Message from {{showuserproper}}</h2>
<p>{{message}}</p>
<br>
{{end}}

View File

@ -0,0 +1,38 @@
<br>
<div class="container">
<img src="{{episodethumbnail}}" alt="{{episodeproper}}" width="120" height="214">
<h1>{{seasonproper}}, {{episodeproper}}</h1>
<h2>{{showproper}}</h2>
<h2>{{userproper}}</h2>
<hr>
{{if episodebrowserexists}}
<a class="btn btn-primary" href="./play">Watch Online</a>
{{end}}
{{if episodeexists}}
<a class="btn btn-primary" href="{{downloadurl}}" download="{{episodeproperattr}}.mp4">Download</a>
{{end}}
{{if editable}}
<a class="btn btn-primary" href="./edit">Edit</a>
<a class="btn btn-primary" href="./subtitles">Edit Subtitles</a>
{{end}}
<br>
<div class="accordion" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Description
</button>
</h2>
<br>
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
<div class="accordion-body">
<p>{{episodedescription}}</p>
</div>
</div>
</div>
</div>
</div>

View File

@ -1,7 +1,7 @@
<h1>Extras path: {{path}}</h1> <h1>Extras path: {{path}}</h1>
{{if editable}} {{if editable}}
<h4>Create Directory</h4> <h4>Create Directory</h4>
<form method="post" enctype="application/x-www-form-urlencoded" action="./mkdir"> <form method="post" enctype="application/x-www-form-urlencoded" action="./mkdir?csrf={{csrf}}">
<input type="hidden" name="parent" value="{{parent}}"> <input type="hidden" name="parent" value="{{parent}}">
<div class="mb-3"> <div class="mb-3">
<label for="exampleFormControlInput1" class="form-label">Directory name</label> <label for="exampleFormControlInput1" class="form-label">Directory name</label>
@ -10,7 +10,7 @@
</div> </div>
</form> </form>
<h4>Upload File</h4> <h4>Upload File</h4>
<form method="post" enctype="multipart/form-data" action="./upload_extra"> <form method="post" enctype="multipart/form-data" action="./upload_extra?csrf={{csrf}}">
<input type="hidden" name="parent" value="{{parent}}"> <input type="hidden" name="parent" value="{{parent}}">
<div class="mb-3"> <div class="mb-3">
<label for="formFileSm" class="form-label">Browse for file</label> <label for="formFileSm" class="form-label">Browse for file</label>

View File

@ -1,4 +1,10 @@
<form action="./mailinglist" method="post"> <form action="./mailinglist" method="post">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="enableupdates" {{if enableupdates}} checked {{end}} id="flexCheckUpdates">
<label class="form-check-label" for="flexCheckUpdates">
Updates
</label>
</div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="enablemovies" {{if enablemovies}} checked {{end}} id="flexCheckMovies"> <input class="form-check-input" type="checkbox" name="enablemovies" {{if enablemovies}} checked {{end}} id="flexCheckMovies">
<label class="form-check-label" for="flexCheckMovies"> <label class="form-check-label" for="flexCheckMovies">
@ -12,6 +18,12 @@
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" name="enablealbums" {{if enablealbums}} checked {{end}} id="flexCheckAlbums">
<label class="form-check-label" for="flexCheckAlbums">
Music Albums
</label>
</div>
<!--<div class="form-check">
<input class="form-check-input" type="checkbox" name="enablesingles" {{if enablesingles}} checked {{end}} id="flexCheckSingle"> <input class="form-check-input" type="checkbox" name="enablesingles" {{if enablesingles}} checked {{end}} id="flexCheckSingle">
<label class="form-check-label" for="flexCheckSingle"> <label class="form-check-label" for="flexCheckSingle">
Music - Single Music - Single
@ -34,6 +46,6 @@
<label class="form-check-label" for="flexCheckOther"> <label class="form-check-label" for="flexCheckOther">
Other Other
</label> </label>
</div> </div>-->
<input type="submit" value="Update" class="btn btn-primary"> <input type="submit" value="Update" class="btn btn-primary">
</form> </form>

View File

@ -4,7 +4,7 @@
<li class="list-group-item"> <li class="list-group-item">
{{user.propername}} ({{user.name}}) {{user.propername}} ({{user.name}})
<form action="./manage" method="post" enctype="application/x-www-form-urlencoded"> <form action="./manage?csrf={{csrf}}" method="post" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="name" value="{{user.nameattr}}"> <input type="hidden" name="name" value="{{user.nameattr}}">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="flexCheckVerified{{user.i}}" name="verified" {{if user.isverified}} checked {{end}}> <input class="form-check-input" type="checkbox" id="flexCheckVerified{{user.i}}" name="verified" {{if user.isverified}} checked {{end}}>

View File

@ -23,7 +23,15 @@
{{if torrentwextraexists}} {{if torrentwextraexists}}
<a class="btn btn-primary" href="{{torrentwextra}}">Torrent With Extras</a> <a class="btn btn-primary" href="{{torrentwextra}}">Torrent With Extras</a>
{{end}} {{end}}
{{if editable}}
<form action="./sendupdate?csrf={{csrf}}" method="post">
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Message To Subscribers (optional)</label>
<textarea name="body" class="form-control" id="exampleFormControlTextarea1" rows="5"></textarea>
</div>
<input type="submit" value="Send Update" class="btn btn-success">
</form>
{{end}}
<br> <br>
<div class="accordion" id="accordionExample"> <div class="accordion" id="accordionExample">
<div class="accordion-item"> <div class="accordion-item">

View File

@ -0,0 +1,289 @@
<style>
#seekbar {
width: 60%;
}
@media (max-width: 1153px) {
#seekbar {
width: 50%;
}
}
@media (max-width: 899px) {
#seekbar {
width: 40%;
}
}
@media (max-width: 737px) {
#seekbar {
width: 30%;
}
}
@media (max-width: 624px) {
#seekbar {
width: 20%;
}
}
@media (max-width: 542px)
{
#seekbar {
width: 10%;
}
}
@media (max-width: 479px)
{
#prev {
display: none;
}
}
#volume
{
width: 5%;
}
#myfooter
{
position: absolute;
display: block;
bottom: 0px;
width: 100%;
padding: 4px;
}
</style>
<div class="row">
<div class="col">
<ul class="list-group" id="tracks">
<!--<li class="list-group-item"><a href="#">Substance</a></li>-->
</ul>
</div>
<div class="col">
<div class="container text-center player-box">
<h1 id="artist"></h1>
<h3 id="album"></h3>
<img height="250" width="250" src="" id="art" alt="">
<h4 id="track"></h4>
<audio id="player"></audio>
</div>
</div>
</div>
<footer id="myfooter" class="bg-dark">
<button onclick="prev_track()" id="prev" class="btn btn-primary"><img src="{{rooturl}}skip-prev.svg" alt="Prev"></button>
<button id="play" onclick="playpause()" class="btn btn-primary"><img id="play_img" src="{{rooturl}}play.svg" alt="Play"></button>
<button onclick="next_track()" id="next" class="btn btn-primary"><img src="{{rooturl}}skip-next.svg" alt="Next"></button>
<span id="curlbl" class="text-light">0:00</span>
<input type="range" onmousedown="seekdown()" onmouseup="seekup()" onmousemove="seekmove()" onclick="seekclick()" ontouchstart="seekdown()" ontouchmove="seekmove()" ontouchend="seekup()" min="0" max="1000" value="0" class="form-range" id="seekbar">
<span id="totallbl" class="text-light">0:00</span>
<input type="range" class="form-range" id="volume" onchange="volume()">
<a class="btn btn-primary" id="download"><img src="{{rooturl}}download.svg" alt="Download"></a>
<button class="btn btn-primary" onclick="repeat_button()"><img id="repeat_icon" src="{{rooturl}}repeat_off.svg" alt="Repeat Off"></button>
<button class="btn btn-primary" onclick="shuffle_button()"><img id="shuffle_icon" src="{{rooturl}}shuffle_off.svg" alt="Shuffle Off"></button>
</footer>
<script>
const list = {{list}};
const track = document.getElementById('track');
const artist = document.getElementById('artist');
const album = document.getElementById('album');
const art = document.getElementById('art');
const player = document.getElementById('player');
const play_img = document.getElementById('play_img');
const curlbl = document.getElementById('curlbl');
const seekbar = document.getElementById('seekbar');
const totallbl = document.getElementById('totallbl');
const repeat_icon = document.getElementById('repeat_icon');
const shuffle_icon = document.getElementById('shuffle_icon');
const tracks = document.getElementById('tracks');
const download = document.getElementById('download');
const prev = document.getElementById('prev');
const _volume = document.getElementById('volume');
var track_index = 0;
_volume.value = player.volume * 100;
function volume()
{
player.volume = _volume.value / 100;
}
function set_track(i)
{
track_index = i;
console.log(track_index);
if(track_index < list.length)
{
player.src = list[track_index].url;
art.src = list[track_index].art;
track.innerText = list[track_index].name;
artist.innerText = list[track_index].artist;
album.innerText = list[track_index].album;
download.href = list[track_index].download;
}
player.play();
}
for(i=0;i<list.length;i++)
{
var li=document.createElement('li');
li.classList.add('list-group-item');
var link = document.createElement('button');
var j= (o)=>{
set_track(o.i);
};
link.onclick = j.bind(this,{i});
link.innerText = list[i].name;
link.classList.add('btn');
link.classList.add('btn-link');
li.appendChild(link);
tracks.appendChild(li);
}
set_track(0);
function next_track()
{
var rpt=parseInt(localStorage.getItem("audio_repeat") ?? "0");
if(rpt === 1) return;
var shuffle=(localStorage.getItem("audio_shuffle") ?? "false") === 'true';
if(shuffle)
{
set_track(Math.floor(Math.random() * list.length));
}
else
{
if(track_index + 1 >= list.length)
{
if(rpt === 2) set_track(0);
}
else
{
set_track(track_index+1);
}
}
}
function shuffle_button()
{
var shuffle=(localStorage.getItem("audio_shuffle") ?? "false") === 'false';
localStorage.setItem('audio_shuffle',shuffle.toString());
set_repeat_button();
}
function prev_track()
{
if(track_index > 0)
{
set_track(track_index-1);
}
else
{
var rpt=parseInt(localStorage.getItem("audio_repeat") ?? "0");
if(rpt === 2)
set_track(list.length-1);
else
set_track(0);
}
}
function set_repeat_button()
{
var shuffle=(localStorage.getItem("audio_shuffle") ?? "false") === 'true';
var rpt=parseInt(localStorage.getItem("audio_repeat") ?? "0");
player.loop = rpt === 1;
shuffle_icon.src = shuffle ? "{{rooturl}}shuffle_on.svg" : "{{rooturl}}shuffle_off.svg";
prev.disabled = shuffle;
switch(rpt)
{
case 0:
repeat_icon.src = "{{rooturl}}repeat_off.svg";
repeat_icon.alt = "Repeat Off";
break;
case 1:
repeat_icon.src = "{{rooturl}}repeat_one.svg";
repeat_icon.alt = "Repeat One";
break;
default:
repeat_icon.src = "{{rooturl}}repeat_all.svg";
repeat_icon.alt = "Repeat All";
break;
}
}
set_repeat_button();
function repeat_button()
{
var rpt=parseInt(localStorage.getItem("audio_repeat") ?? "0");
rpt = (rpt+1) % 3;
localStorage.setItem("audio_repeat",rpt.toString());
set_repeat_button();
}
var down=false;
function seekdown()
{
down=true;
player.currentTime = (seekbar.value / 1000) * player.duration;
}
function seekup()
{
down=false;
}
function seekmove()
{
if(down)
player.currentTime = (seekbar.value / 1000) * player.duration;
}
function seekclick()
{
player.currentTime = (seekbar.value / 1000) * player.duration;
}
player.ondurationchange = () => {
var sec= new Number();
var min= new Number();
sec = Math.floor( player.duration );
min = Math.floor( sec / 60 );
sec = Math.floor( sec % 60 );
sec = sec >= 10 ? sec : '0' + sec;
totallbl.innerText = `${min}:${sec}`;
}
player.onplay = () => {
play_img.src = "{{rooturl}}pause.svg";
play_img.alt = "Pause";
}
player.onpause = () => {
play_img.src = "{{rooturl}}play.svg";
play_img.alt = "Play";
}
player.onended = ()=>{
next_track();
}
player.ontimeupdate = () => {
var curTime=player.currentTime;
var totalTime = player.duration;
var myOffset = (curTime / totalTime) * 1000;
if(!down)
seekbar.value = myOffset;
/*https://stackoverflow.com/a/21930876*/
var sec= new Number();
var min= new Number();
sec = Math.floor( curTime );
min = Math.floor( sec / 60 );
sec = Math.floor( sec % 60 );
sec = sec >= 10 ? sec : '0' + sec;
curlbl.innerText = `${min}:${sec}`;
};
function playpause()
{
if(player.paused) player.play(); else player.pause();
}
</script>

View File

@ -5,6 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title> <title>{{title}}</title>
<link rel="stylesheet" href="{{rooturl}}bootstrap.min.css"> <link rel="stylesheet" href="{{rooturl}}bootstrap.min.css">
<link rel="apple-touch-icon" sizes="180x180" href="{{rooturl}}apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="{{rooturl}}favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="{{rooturl}}favicon-16x16.png">
<link rel="manifest" href="{{rooturl}}site.webmanifest">
<style> <style>
.wrapped-link { .wrapped-link {
width: 120px; width: 120px;

Some files were not shown because too many files have changed in this diff Show More