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
|
||||
*.swp
|
||||
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
|
||||
COPY . .
|
||||
WORKDIR /src/Tesses.CMS.Server
|
||||
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
|
||||
WORKDIR /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 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(item.ProperName);
|
||||
Console.WriteLine($"\t{res.PosterUrl}");
|
||||
Console.WriteLine("About to read events");
|
||||
using(var cms = new TessesCMSClient(prefs.Url))
|
||||
{
|
||||
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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="ReadLine" Version="2.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
|
|
|
@ -2,17 +2,36 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Converters;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Tesses.CMS.Client
|
||||
{
|
||||
public class TessesCMSClient
|
||||
public enum TessesCMSContentType
|
||||
{
|
||||
Movie,
|
||||
|
||||
Show,
|
||||
|
||||
Album,
|
||||
|
||||
MusicVideo,
|
||||
|
||||
SoftwareProject,
|
||||
|
||||
Other
|
||||
}
|
||||
public class TessesCMSClient : IDisposable
|
||||
{
|
||||
|
||||
private static HttpClient CreateHttpClient()
|
||||
{
|
||||
HttpClientHandler httpClientHandler=new HttpClientHandler();
|
||||
|
@ -24,28 +43,88 @@ namespace Tesses.CMS.Client
|
|||
internal HttpClient client;
|
||||
internal string rooturl;
|
||||
public HttpClient Client => client;
|
||||
public string RootUrl => $"{rooturl}/";
|
||||
public TessesCMSClient(string url,HttpClient client)
|
||||
public string RootUrl
|
||||
{
|
||||
get=>$"{rooturl}/";
|
||||
set {
|
||||
rooturl = value.TrimEnd('/');
|
||||
}
|
||||
}
|
||||
bool ownsClient;
|
||||
public TessesCMSClient(string url,HttpClient client,bool ownsClient)
|
||||
{
|
||||
this.ownsClient=ownsClient;
|
||||
this.client = client;
|
||||
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)
|
||||
{
|
||||
using(var f = File.Create(dest))
|
||||
using(var f = File.Open(dest,FileMode.OpenOrCreate,FileAccess.Write))
|
||||
await DownloadFileAsync(url,f,token,progress);
|
||||
}
|
||||
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);
|
||||
resp.Dispose();
|
||||
}
|
||||
|
||||
public ShowClient Shows => new ShowClient(this);
|
||||
public MovieClient Movies => new MovieClient(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
|
||||
{
|
||||
TessesCMSClient client;
|
||||
|
@ -95,21 +251,115 @@ namespace Tesses.CMS.Client
|
|||
{
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
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>();
|
||||
us.Add("email",email);
|
||||
us.Add("password",password);
|
||||
using(var res=await client.client.PostAsync($"{client.rooturl}/login", new FormUrlEncodedContent(us)))
|
||||
return res.StatusCode != System.Net.HttpStatusCode.Unauthorized;
|
||||
us.Add("type","json");
|
||||
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;
|
||||
internal MovieClient(TessesCMSClient client)
|
||||
|
@ -118,7 +368,7 @@ namespace Tesses.CMS.Client
|
|||
}
|
||||
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;
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
[JsonProperty("has_movie_torrent")]
|
||||
public bool HasMovieTorrent{get;set;}
|
||||
[JsonProperty("movie_torrent_url")]
|
||||
public string MovieTorrentUrl {get;set;}
|
||||
|
||||
[JsonProperty("has_movie_with_extras_torrent")]
|
||||
public bool HasMovieWithExtrasTorrent{get;set;}
|
||||
[JsonProperty("movie_with_extras_torrent_url")]
|
||||
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")]
|
||||
public string BrowserStream {get;set;}
|
||||
|
||||
|
||||
[JsonProperty("download_stream")]
|
||||
public string DownloadStream {get;set;}
|
||||
[JsonProperty("has_poster")]
|
||||
public bool HasPoster {get;set;}
|
||||
|
||||
[JsonProperty("poster_url")]
|
||||
public string PosterUrl {get;set;}
|
||||
|
||||
[JsonProperty("has_thumbnail")]
|
||||
public bool HasThumbnail {get;set;}
|
||||
|
||||
[JsonProperty("thumbnail_url")]
|
||||
|
||||
public string ThumbnailUrl {get;set;}
|
||||
|
@ -56,14 +69,15 @@ namespace Tesses.CMS.Client
|
|||
|
||||
public class Movie
|
||||
{
|
||||
[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,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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
var userId=GetUserAccount(user).Id;
|
||||
|
@ -59,11 +64,21 @@ namespace Tesses.CMS.Providers
|
|||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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});
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users WHERE Username=@user;",new{user})?.Account;
|
||||
|
@ -170,12 +210,32 @@ namespace Tesses.CMS.Providers
|
|||
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)
|
||||
{
|
||||
DapperMovie dapperMovie=new DapperMovie(movie);
|
||||
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)
|
||||
{
|
||||
DapperUserAccount account1=new DapperUserAccount(account);
|
||||
|
|
|
@ -18,17 +18,13 @@ public class LiteDBContentProvider : IContentProvider
|
|||
{
|
||||
var userId=GetUserAccount(user).Id;
|
||||
|
||||
Movie _movie = new Movie(){UserId = userId,Name = movie,ProperName=properName,Description = description};
|
||||
_movie.Id=Movies.Insert(_movie);
|
||||
return _movie;
|
||||
return CreateMovie(userId,movie,properName,description);
|
||||
}
|
||||
public Show CreateShow(string user, string show, string properName, string description)
|
||||
{
|
||||
var userId=GetUserAccount(user).Id;
|
||||
|
||||
Show _show = new Show(){UserId = userId,Name = show,ProperName=properName,Description = description};
|
||||
_show.Id=Shows.Insert(_show);
|
||||
return _show;
|
||||
return CreateShow(userId,show,properName,description);
|
||||
}
|
||||
private ILiteCollection<UserAccount> UserAccounts => db.GetCollection<UserAccount>("users");
|
||||
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<LiteDbVerificationToken> VerificationCodes => db.GetCollection<LiteDbVerificationToken>("verificationcodes");
|
||||
|
||||
private ILiteCollection<Album> Albums => db.GetCollection<Album>("albums");
|
||||
public UserAccount GetFirstUser()
|
||||
{
|
||||
return GetUsers().First();
|
||||
|
@ -105,10 +101,12 @@ public class LiteDBContentProvider : IContentProvider
|
|||
|
||||
public void UpdateMovie(Movie movie)
|
||||
{
|
||||
movie.LastUpdated = DateTime.Now;
|
||||
Movies.Update(movie);
|
||||
}
|
||||
public void UpdateShow(Show show)
|
||||
{
|
||||
show.LastUpdated = DateTime.Now;
|
||||
Shows.Update(show);
|
||||
}
|
||||
|
||||
|
@ -210,13 +208,7 @@ public class LiteDBContentProvider : IContentProvider
|
|||
var myShow = GetShow(user,show);
|
||||
var userId = myShow.UserId;
|
||||
var showId = myShow.Id;
|
||||
int seasonLargest=0;
|
||||
foreach(var item in Seasons.Find(e=>e.ShowId==showId && e.UserId == userId))
|
||||
{
|
||||
if(item.SeasonNumber > seasonLargest)
|
||||
seasonLargest = item.SeasonNumber;
|
||||
}
|
||||
return seasonLargest;
|
||||
return SeasonCount(userId,showId);
|
||||
}
|
||||
|
||||
public Season GetSeason(string user, string show, int season)
|
||||
|
@ -224,7 +216,7 @@ public class LiteDBContentProvider : IContentProvider
|
|||
var myShow = GetShow(user,show);
|
||||
var userId = myShow.UserId;
|
||||
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)
|
||||
|
@ -232,9 +224,7 @@ public class LiteDBContentProvider : IContentProvider
|
|||
var myShow = GetShow(user,show);
|
||||
var userId = myShow.UserId;
|
||||
var showId = myShow.Id;
|
||||
Season _season = new Season(){UserId = userId,ShowId = showId,ProperName=properName,Description = description, SeasonNumber=season};
|
||||
_season.Id=Seasons.Insert(_season);
|
||||
return _season;
|
||||
return CreateSeason(userId,showId,season,properName,description);
|
||||
}
|
||||
|
||||
public int EpisodeCount(string user, string show, int season)
|
||||
|
@ -242,22 +232,16 @@ public class LiteDBContentProvider : IContentProvider
|
|||
var myShow = GetShow(user,show);
|
||||
var userId = myShow.UserId;
|
||||
var showId = myShow.Id;
|
||||
int episodeLargest=0;
|
||||
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;
|
||||
return EpisodeCount(userId,showId,season);
|
||||
}
|
||||
|
||||
public Episode GetEpisode(string user, string show, int season, int episode)
|
||||
{
|
||||
var myShow = GetShow(user,show);
|
||||
|
||||
var userId = myShow.UserId;
|
||||
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)
|
||||
|
@ -265,19 +249,136 @@ public class LiteDBContentProvider : IContentProvider
|
|||
var myShow = GetShow(user,show);
|
||||
var userId = myShow.UserId;
|
||||
var showId = myShow.Id;
|
||||
Episode _episode = new Episode(){UserId = userId,ShowId = showId,ProperName=properName,Description = description, SeasonNumber=season, EpisodeNumber = episode,EpisodeName=episodename};
|
||||
_episode.Id=Episodes.Insert(_episode);
|
||||
return _episode;
|
||||
return CreateEpisode(userId,showId,season,episode,episodename,properName,description);
|
||||
}
|
||||
|
||||
public void UpdateEpisode(Episode episode)
|
||||
{
|
||||
episode.LastUpdated = DateTime.Now;
|
||||
Episodes.Update(episode);
|
||||
}
|
||||
|
||||
public void UpdateSeason(Season season)
|
||||
{
|
||||
season.LastUpdated = DateTime.Now;
|
||||
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>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
_=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>
|
||||
<li><a href="{{rooturl}}api/v1/">Api</a></li>
|
||||
<li><a href="{{rooturl}}devcenter/webhooks">Webhooks</a></li>
|
||||
</ul>
|
||||
</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>
|
||||
<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">
|
||||
<label for="proper-name" class="form-label">Proper Name</label>
|
||||
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
|
||||
|
@ -12,7 +12,7 @@
|
|||
|
||||
</form>
|
||||
<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>
|
||||
<select class="form-select" name="type" aria-label="Select resource">
|
||||
<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">
|
||||
<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 show (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="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 season (for when episode does not have one), what you see on video player before pressing play)</option>
|
||||
</select>
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
{{if editable}}
|
||||
<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}}">
|
||||
<div class="mb-3">
|
||||
<label for="exampleFormControlInput1" class="form-label">Directory name</label>
|
||||
|
@ -10,7 +10,7 @@
|
|||
</div>
|
||||
</form>
|
||||
<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}}">
|
||||
<div class="mb-3">
|
||||
<label for="formFileSm" class="form-label">Browse for file</label>
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
<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">
|
||||
<input class="form-check-input" type="checkbox" name="enablemovies" {{if enablemovies}} checked {{end}} id="flexCheckMovies">
|
||||
<label class="form-check-label" for="flexCheckMovies">
|
||||
|
@ -12,6 +18,12 @@
|
|||
</label>
|
||||
</div>
|
||||
<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">
|
||||
<label class="form-check-label" for="flexCheckSingle">
|
||||
Music - Single
|
||||
|
@ -34,6 +46,6 @@
|
|||
<label class="form-check-label" for="flexCheckOther">
|
||||
Other
|
||||
</label>
|
||||
</div>
|
||||
</div>-->
|
||||
<input type="submit" value="Update" class="btn btn-primary">
|
||||
</form>
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<li class="list-group-item">
|
||||
{{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}}">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="flexCheckVerified{{user.i}}" name="verified" {{if user.isverified}} checked {{end}}>
|
||||
|
|
|
@ -23,7 +23,15 @@
|
|||
{{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>
|
||||
<div class="accordion" id="accordionExample">
|
||||
<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">
|
||||
<title>{{title}}</title>
|
||||
<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>
|
||||
.wrapped-link {
|
||||
width: 120px;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue