Fixed video player in app for desktop
This commit is contained in:
parent
f81fb656e8
commit
b3ae68232d
|
@ -483,3 +483,4 @@ $RECYCLE.BIN/
|
||||||
# Vim temporary swap files
|
# Vim temporary swap files
|
||||||
*.swp
|
*.swp
|
||||||
help.txt
|
help.txt
|
||||||
|
data/
|
||||||
|
|
|
@ -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 .
|
||||||
|
|
|
@ -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
|
|
@ -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 |
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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.
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="splash_background">#212121</color>
|
||||||
|
</resources>
|
|
@ -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>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="splash_background">#FFFFFF</color>
|
||||||
|
</resources>
|
|
@ -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>
|
|
@ -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)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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>
|
|
@ -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";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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
|
|
@ -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>
|
|
@ -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 |
|
@ -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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Tesses.CMS.Avalonia.ViewModels;
|
||||||
|
|
||||||
|
public interface IBackable
|
||||||
|
{
|
||||||
|
public ViewModelBase Back();
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.ViewModels;
|
||||||
|
|
||||||
|
public class ViewModelBase : ObservableObject
|
||||||
|
{
|
||||||
|
}
|
|
@ -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>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class FavoritesPageView : UserControl
|
||||||
|
{
|
||||||
|
public FavoritesPageView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views.HomePages;
|
||||||
|
|
||||||
|
public partial class HomeMovieListPageView : UserControl
|
||||||
|
{
|
||||||
|
public HomeMovieListPageView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}"><-Back</Button>
|
||||||
|
<TransitioningContentControl Grid.Row="1" Content="{Binding CurrentPage}" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class HomePageView : UserControl
|
||||||
|
{
|
||||||
|
public HomePageView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views.HomePages;
|
||||||
|
|
||||||
|
public partial class HomeUserListPageView : UserControl
|
||||||
|
{
|
||||||
|
public HomeUserListPageView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views.HomePages;
|
||||||
|
|
||||||
|
public partial class HomeUserPageView : UserControl
|
||||||
|
{
|
||||||
|
public HomeUserPageView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class MainView : UserControl
|
||||||
|
{
|
||||||
|
public MainView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class NotificationsPageView : UserControl
|
||||||
|
{
|
||||||
|
public NotificationsPageView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,11 @@
|
||||||
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
namespace Tesses.CMS.Avalonia.Views;
|
||||||
|
|
||||||
|
public partial class SettingsPageView : UserControl
|
||||||
|
{
|
||||||
|
public SettingsPageView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
{
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;}="";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;}="";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;}="";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;}="";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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}}
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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}}>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue