Add project files.

This commit is contained in:
2025-06-29 08:47:52 +02:00
commit 88161dc2ab
46 changed files with 1894 additions and 0 deletions

63
.gitattributes vendored Normal file
View File

@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

363
.gitignore vendored Normal file
View File

@ -0,0 +1,363 @@
## 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/
[Oo]ut/
[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/
# 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

12
OfflineDemo/App.razor Normal file
View File

@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@ -0,0 +1,12 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<article class="content px-4">
@Body
</article>
</main>
</div>

View File

@ -0,0 +1,78 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background: rgb(1,26,113);
background: linear-gradient(180deg, rgba(1,26,113,1) 0%, rgba(17,48,157,1) 100%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}

View File

@ -0,0 +1,44 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">OfflineDemo</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
</div>
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door mx-2" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="todo">
<span class="bi bi-list-task mx-2" aria-hidden="true"></span> ToDo List
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="shorts">
<i class="bi bi-card-checklist mx-2" aria-hidden="true"></i> Shorts
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="upload">
<span class="bi bi-cloud-arrow-up mx-2" aria-hidden="true"></span> Upload
</NavLink>
</div>
</nav>
</div>
@code {
private bool collapseNavMenu = true;
private string? NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}

View File

@ -0,0 +1,56 @@
.navbar-toggler {
background-color: rgba(255, 255, 255, 0.1);
}
.navbar-brand {
font-size: 1.1rem;
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep a {
color: #d7d7d7;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep a:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.collapse {
/* Never collapse the sidebar for wide screens */
display: block;
}
.nav-scrollable {
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@ -0,0 +1,12 @@
namespace OfflineDemo.Models;
public class ShortsModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Hashtags { get; set; }
public string Mp4FileUrl { get; set; }
public string ImageFileUrl { get; set; }
public bool IsUploaded { get; set; } = false;
}

View File

@ -0,0 +1,8 @@
namespace OfflineDemo.Models;
public class TodoModel
{
public DateTime CreationDate { get; set; } = DateTime.Now;
public string ToDoItem { get; set; }
public bool IsComplete { get; set; }
}

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace OfflineDemo.Models;
public class UploadModel
{
[Required]
[StringLength(100, ErrorMessage = "Title must be less than 100 characters.")]
public string Title { get; set; } = "";
[Required]
[StringLength(500, ErrorMessage = "Description must be less than 500 characters.")]
public string Description { get; set; } = "";
[StringLength(200, ErrorMessage = "Hashtags must be less than 200 characters.")]
public string Hashtags { get; set; } = "#";
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel.Core" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@ -0,0 +1,62 @@
@page "/shorts"
@inject HttpClient httpClient
@inject IConfiguration config
<h3>Shorts List</h3>
@if (shorts == null)
{
<p>Loading...</p>
}
else if (!shorts.Any())
{
<p>No shorts available.</p>
}
else
{
<table class="table table-striped">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Description</th>
<th>Hashtags</th>
<th>Mp4 URL</th>
<th>Image URL</th>
<th>Uploaded</th>
</tr>
</thead>
<tbody>
@foreach (var shortItem in shorts)
{
<tr>
<td>@shortItem.Id</td>
<td>@shortItem.Title</td>
<td>@shortItem.Description</td>
<td>@shortItem.Hashtags</td>
<td><a href="@shortItem.Mp4FileUrl" target="_blank">View Video</a></td>
<td><a href="@shortItem.ImageFileUrl" target="_blank">View Image</a></td>
<td>@(shortItem.IsUploaded ? "Yes" : "No")</td>
</tr>
}
</tbody>
</table>
}
@code {
private List<ShortsModel>? shorts;
protected override async Task OnInitializedAsync()
{
try
{
string url = $"{config["ApiUrl"]}/shorts";
shorts = await httpClient.GetFromJsonAsync<List<ShortsModel>>(url);
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error fetching shorts: {ex.Message}");
}
}
}

View File

@ -0,0 +1,138 @@
@page "/upload"
@using Microsoft.AspNetCore.Components.Forms
@using System.ComponentModel.DataAnnotations
@inject HttpClient http
@inject IConfiguration config
@inject NavigationManager nav
<h3>Upload TikTok Video</h3>
@if (isSubmitting)
{
<p>Uploading...</p>
}
@if (!string.IsNullOrEmpty(resultMessage))
{
<p>@resultMessage</p>
}
<EditForm Model="@uploadModel" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group mb-3">
<label class="form-label" for="title">Title</label>
<InputText id="title" class="form-control" @bind-Value="uploadModel.Title" />
<ValidationMessage For="@(() => uploadModel.Title)" />
</div>
<div class="form-group mb-3">
<label class="form-label" for="description">Description</label>
<InputTextArea id="description" class="form-control" @bind-Value="uploadModel.Description" />
<ValidationMessage For="@(() => uploadModel.Description)" />
</div>
<div class="form-group mb-3">
<label class="form-label" for="hashtags">Hashtags</label>
<InputText id="hashtags" @oninput="HandleHashtagInput" class="form-control" @bind-Value="uploadModel.Hashtags" />
<ValidationMessage For="@(() => uploadModel.Hashtags)" />
</div>
<div class="form-group mb-3">
<label class="form-label" for="mp4Upload">MP4 File</label>
<InputFile id="mp4Upload" class="form-control" OnChange="@HandleMp4FileChange" accept=".mp4,.mov" />
</div>
<div class="form-group mb-3">
<label class="form-label" for="imageUpload">Image File (optional)</label>
<InputFile id="imageUpload" class="form-control" OnChange="@HandleImageFileChange" accept=".jpg,.jpeg,.png" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</EditForm>
@code {
private UploadModel uploadModel = new();
private IBrowserFile? mp4File;
private IBrowserFile? imageFile;
private bool isSubmitting = false;
private string? resultMessage;
private void HandleMp4FileChange(InputFileChangeEventArgs e)
{
mp4File = e.File;
}
private void HandleImageFileChange(InputFileChangeEventArgs e)
{
imageFile = e.File;
}
private async Task HandleHashtagInput(ChangeEventArgs e)
{
var input = e.Value?.ToString() ?? "";
if (input.EndsWith(" "))
{
// Add a hashtag before the space
uploadModel.Hashtags = input.TrimEnd() + " #";
// Optionally, reset cursor position
await InvokeAsync(StateHasChanged);
}
}
private async Task HandleValidSubmit()
{
resultMessage = "";
if (mp4File == null || imageFile == null)
{
resultMessage = "Both MP4 and Image files are required.";
return;
}
isSubmitting = true;
try
{
using var content = new MultipartFormDataContent();
content.Add(new StringContent(uploadModel.Title ?? string.Empty), "Title");
content.Add(new StringContent(uploadModel.Description ?? string.Empty), "Description");
content.Add(new StringContent(uploadModel.Hashtags ?? string.Empty), "Hashtags");
// Add files
var mp4Stream = mp4File.OpenReadStream(800 * 1024 * 1024); // 800 MB limit
var mp4Content = new StreamContent(mp4Stream);
mp4Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(mp4File.ContentType);
content.Add(mp4Content, "Mp4File", mp4File.Name);
var imageStream = imageFile.OpenReadStream(10 * 1024 * 1024); // 10 MB limit for images
var imageContent = new StreamContent(imageStream);
imageContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(imageFile.ContentType);
content.Add(imageContent, "ImageFile", imageFile.Name);
// Call the API
string url = $"{config["ApiUrl"]}/upload";
var response = await http.PostAsync(url, content);
if (response.IsSuccessStatusCode)
{
resultMessage = "Files uploaded successfully!";
nav.NavigateTo("/shorts");
}
else
{
var error = await response.Content.ReadAsStringAsync();
resultMessage = $"Error: {response.StatusCode} - {error}";
}
}
catch (Exception ex)
{
resultMessage = $"An error occurred: {ex.Message}";
}
finally
{
isSubmitting = false;
}
}
}

View File

@ -0,0 +1,49 @@
@page "/todo"
@inject ILocalStorageService localStorage
<h3>ToDo</h3>
@if (todos is not null)
{
<ol>
@foreach (var todo in todos)
{
<li>@todo.ToDoItem (@todo.CreationDate.ToString("MMMM dd, yyyy hh:mm tt"))</li>
}
</ol>
}
<h4>Add ToDo Item</h4>
<EditForm Model="newTodo" OnValidSubmit="AddTodo">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="newTodo.ToDoItem">ToDo Item</label>
<InputText id="newTodo.ToDoItem" class="form-control" @bind-Value="newTodo.ToDoItem" />
<ValidationMessage For="@(() => newTodo.ToDoItem)" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
</EditForm>
@code {
private List<TodoModel> todos;
private TodoModel newTodo = new();
protected override async Task OnInitializedAsync()
{
todos = await localStorage.GetItemAsync<List<TodoModel>>("todos");
if (todos is null || todos.Count == 0)
{
todos = new();
await localStorage.SetItemAsync("todos", todos);
}
}
private async Task AddTodo()
{
todos.Add(newTodo);
newTodo = new();
await localStorage.SetItemAsync("todos", todos);
}
}

19
OfflineDemo/Program.cs Normal file
View File

@ -0,0 +1,19 @@
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using OfflineDemo;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient());
builder.Services.AddBlazoredLocalStorage();
builder.Services.Configure<KestrelServerOptions>(options =>
{
options.Limits.MaxRequestBodySize = 800L * 1024 * 1024; // 800 MB
});
await builder.Build().RunAsync();

View File

@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:28724",
"sslPort": 44360
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5137",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://localhost:7119;http://localhost:5137",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,12 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using OfflineDemo
@using OfflineDemo.Layout
@using Blazored.LocalStorage
@using OfflineDemo.Models

View File

@ -0,0 +1,8 @@
{
"Kestrel": {
"Limits": {
"MaxRequestBodySize": 838860800 // 800 MB in bytes
}
},
"ApiUrl": "https://localhost:7099/api"
}

View File

@ -0,0 +1,103 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
h1:focus {
outline: none;
}
a, .btn-link {
color: #0071c1;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid red;
}
.validation-message {
color: red;
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.loading-progress {
position: relative;
display: block;
width: 8rem;
height: 8rem;
margin: 20vh auto 1rem auto;
}
.loading-progress circle {
fill: none;
stroke: #e0e0e0;
stroke-width: 0.6rem;
transform-origin: 50% 50%;
transform: rotate(-90deg);
}
.loading-progress circle:last-child {
stroke: #1b6ec2;
stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
transition: stroke-dasharray 0.05s ease-in-out;
}
.loading-progress-text {
position: absolute;
text-align: center;
font-weight: bold;
inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
}
.loading-progress-text:after {
content: var(--blazor-load-percentage-text, "Loading");
}
code {
color: #c02d76;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OfflineDemo</title>
<base href="/" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<link rel="stylesheet" href="css/app.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<link href="OfflineDemo.styles.css" rel="stylesheet" />
<link href="manifest.webmanifest" rel="manifest" />
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
</head>
<body>
<div id="app">
<svg class="loading-progress">
<circle r="40%" cx="50%" cy="50%" />
<circle r="40%" cx="50%" cy="50%" />
</svg>
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script>
</body>
</html>

View File

@ -0,0 +1,22 @@
{
"name": "OfflineDemo",
"short_name": "OfflineDemo",
"id": "./",
"start_url": "./",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#03173d",
"prefer_related_applications": false,
"icons": [
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "icon-192.png",
"type": "image/png",
"sizes": "192x192"
}
]
}

View File

@ -0,0 +1,27 @@
[
{
"date": "2022-01-06",
"temperatureC": 1,
"summary": "Freezing"
},
{
"date": "2022-01-07",
"temperatureC": 14,
"summary": "Bracing"
},
{
"date": "2022-01-08",
"temperatureC": -13,
"summary": "Freezing"
},
{
"date": "2022-01-09",
"temperatureC": -16,
"summary": "Balmy"
},
{
"date": "2022-01-10",
"temperatureC": -2,
"summary": "Chilly"
}
]

View File

@ -0,0 +1,4 @@
// In development, always fetch from the network and do not enable offline support.
// This is because caching would make development more difficult (changes would not
// be reflected on the first load after each change).
self.addEventListener('fetch', () => { });

View File

@ -0,0 +1,55 @@
// Caution! Be sure you understand the caveats before publishing an application with
// offline support. See https://aka.ms/blazor-offline-considerations
self.importScripts('./service-worker-assets.js');
self.addEventListener('install', event => event.waitUntil(onInstall(event)));
self.addEventListener('activate', event => event.waitUntil(onActivate(event)));
self.addEventListener('fetch', event => event.respondWith(onFetch(event)));
const cacheNamePrefix = 'offline-cache-';
const cacheName = `${cacheNamePrefix}${self.assetsManifest.version}`;
const offlineAssetsInclude = [ /\.dll$/, /\.pdb$/, /\.wasm/, /\.html/, /\.js$/, /\.json$/, /\.css$/, /\.woff$/, /\.png$/, /\.jpe?g$/, /\.gif$/, /\.ico$/, /\.blat$/, /\.dat$/ ];
const offlineAssetsExclude = [ /^service-worker\.js$/ ];
// Replace with your base path if you are hosting on a subfolder. Ensure there is a trailing '/'.
const base = "/";
const baseUrl = new URL(base, self.origin);
const manifestUrlList = self.assetsManifest.assets.map(asset => new URL(asset.url, baseUrl).href);
async function onInstall(event) {
console.info('Service worker: Install');
// Fetch and cache all matching items from the assets manifest
const assetsRequests = self.assetsManifest.assets
.filter(asset => offlineAssetsInclude.some(pattern => pattern.test(asset.url)))
.filter(asset => !offlineAssetsExclude.some(pattern => pattern.test(asset.url)))
.map(asset => new Request(asset.url, { integrity: asset.hash, cache: 'no-cache' }));
await caches.open(cacheName).then(cache => cache.addAll(assetsRequests));
}
async function onActivate(event) {
console.info('Service worker: Activate');
// Delete unused caches
const cacheKeys = await caches.keys();
await Promise.all(cacheKeys
.filter(key => key.startsWith(cacheNamePrefix) && key !== cacheName)
.map(key => caches.delete(key)));
}
async function onFetch(event) {
let cachedResponse = null;
if (event.request.method === 'GET') {
// For all navigation requests, try to serve index.html from cache,
// unless that request is for an offline resource.
// If you need some URLs to be server-rendered, edit the following check to exclude those URLs
const shouldServeIndexHtml = event.request.mode === 'navigate'
&& !manifestUrlList.some(url => url === event.request.url);
const request = shouldServeIndexHtml ? 'index.html' : event.request;
const cache = await caches.open(cacheName);
cachedResponse = await cache.match(request);
}
return cachedResponse || fetch(event.request);
}

View File

@ -0,0 +1,11 @@

namespace OfflineDemoApi.Data
{
public interface ISqlDataAccess
{
Task<List<T>> LoadData<T, U>(string storedProcedure, U parameters, string connectionStringName);
Task SaveData<T>(string storedProcedure, T parameters, string connectionStringName);
Task<T> SaveDataScalar<T, U>(string storedProcedure, U parameters, string connectionStringName);
}
}

View File

@ -0,0 +1,58 @@
using Dapper;
using Microsoft.Data.SqlClient;
using System.Data;
namespace OfflineDemoApi.Data;
public class SqlDataAccess : ISqlDataAccess
{
private readonly IConfiguration _config;
public SqlDataAccess(IConfiguration config)
{
_config = config;
}
public async Task<List<T>> LoadData<T, U>(string storedProcedure,
U parameters,
string connectionStringName)
{
string connectionString = _config.GetConnectionString(connectionStringName) ??
throw new KeyNotFoundException("Did not find the connection string specified");
using IDbConnection connection = new SqlConnection(connectionString);
List<T> output = (await connection.QueryAsync<T>(
storedProcedure,
parameters,
commandType: CommandType.StoredProcedure)).ToList();
return output;
}
public async Task<T> SaveDataScalar<T, U>(string storedProcedure,
U parameters,
string connectionStringName)
{
string connectionString = _config.GetConnectionString(connectionStringName) ??
throw new KeyNotFoundException("Did not find the connection string specified");
using IDbConnection connection = new SqlConnection(connectionString);
T? output = await connection.ExecuteScalarAsync<T>(storedProcedure, parameters, commandType: CommandType.StoredProcedure);
return output ?? throw new ArgumentNullException("The return value was null, which is an invalid result.");
}
public async Task SaveData<T>(string storedProcedure,
T parameters,
string connectionStringName)
{
string connectionString = _config.GetConnectionString(connectionStringName) ??
throw new KeyNotFoundException("Did not find the connection string specified");
using IDbConnection connection = new SqlConnection(connectionString);
await connection.ExecuteAsync(storedProcedure, parameters, commandType: CommandType.StoredProcedure);
}
}

View File

@ -0,0 +1,7 @@
namespace OfflineDemoApi.Models;
public class AzureStorageConfig
{
public string? ConnectionString { get; set; }
public string? ContainerName { get; set; }
}

View File

@ -0,0 +1,12 @@
namespace OfflineDemoApi.Models;
public class ShortsModel
{
public int Id { get; set; }
public string Title { get; set; }
public string Description { get; set; }
public string Hashtags { get; set; }
public string Mp4FileUrl { get; set; }
public string ImageFileUrl { get; set; }
public bool IsUploaded { get; set; } = false;
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>9343a209-53dc-41cc-b263-bce9f7c1f999</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Storage.Blobs" Version="12.23.0" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>
</Project>

145
OfflineDemoApi/Program.cs Normal file
View File

@ -0,0 +1,145 @@
using Azure.Storage.Blobs;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using OfflineDemoApi.Data;
using OfflineDemoApi.Models;
using System;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
// Bind Azure Storage configuration
var azureStorageConfig = builder.Configuration.GetSection("AzureStorage").Get<AzureStorageConfig>();
// Ensure the blob container exists
var blobServiceClient = new BlobServiceClient(azureStorageConfig.ConnectionString);
var blobContainerClient = blobServiceClient.GetBlobContainerClient(azureStorageConfig.ContainerName);
await blobContainerClient.CreateIfNotExistsAsync();
await blobContainerClient.SetAccessPolicyAsync(Azure.Storage.Blobs.Models.PublicAccessType.None);
builder.Services.AddSingleton(blobContainerClient);
// Configure Kestrel limits
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 524288000*2; // Example: 500 MB
options.Limits.MaxRequestBufferSize = 524288000 * 2; // Example: 100 MB
});
builder.Services.AddSingleton<ISqlDataAccess, SqlDataAccess>();
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowAnyOrigin", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod();
});
});
var app = builder.Build();
app.UseCors("AllowAnyOrigin");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.MapGet("/", () => {
return "Hello World";
});
app.MapGet("/api/shorts", async (ISqlDataAccess sql) =>
{
return await sql.LoadData<ShortsModel, dynamic>("spShorts_GetAll", new { }, "sql");
});
app.MapGet("/api/file", async (IConfiguration config, string url) =>
{
var blobUri = new Uri(url);
var storageAccountKey = config.GetValue<string>("AzureStorage:StorageAccountKey");
var storageAccountName = config.GetValue<string>("AzureStorage:StorageAccountName");
var credential = new Azure.Storage.StorageSharedKeyCredential(storageAccountName, storageAccountKey);
var blobClient = new BlobClient(blobUri, credential);
var downloadResponse = await blobClient.DownloadStreamingAsync();
return Results.File(
downloadResponse.Value.Content,
downloadResponse.Value.Details.ContentType,
fileDownloadName: blobUri.Segments.Last()
);
});
app.MapPost("/api/upload", async (HttpRequest request,
BlobContainerClient containerClient,
ISqlDataAccess sql) =>
{
if (!request.HasFormContentType || request.Form.Files.Count == 0)
{
return Results.BadRequest("Invalid form submission. Files are required.");
}
var form = await request.ReadFormAsync();
// Extract form data
var title = form["Title"].ToString();
var description = form["Description"].ToString();
var hashtags = form["Hashtags"].ToString();
var mp4File = form.Files["Mp4File"];
var imageFile = form.Files["ImageFile"];
if (mp4File == null || imageFile == null)
{
return Results.BadRequest("Both MP4 and image files are required.");
}
// Validate file size
const long maxFileSize = 800L * 1024 * 1024; // 800MB
if (mp4File.Length > maxFileSize)
{
return Results.BadRequest("MP4 file exceeds the 800MB limit.");
}
var createNewParameters = new { title, description, hashtags };
int id = await sql.SaveDataScalar<int, dynamic>("spShorts_CreateNew", createNewParameters, "sql");
// Rename files
string? mp4FileName = $"{id}.mp4";
// TODO - Fix this extension lookup
string? imageFileName = $"{id}.{imageFile.FileName.Split('.')[1]}";
// Upload MP4 file
var mp4BlobClient = containerClient.GetBlobClient($"shorts/{mp4FileName}");
await using (var mp4Stream = mp4File.OpenReadStream())
{
await mp4BlobClient.UploadAsync(mp4Stream, true);
}
// Upload Image file
var imageBlobClient = containerClient.GetBlobClient($"images/{imageFileName}");
await using (var imageStream = imageFile.OpenReadStream())
{
await imageBlobClient.UploadAsync(imageStream, true);
}
// Update SQL with the uploaded files
string? mp4FileUrl = mp4BlobClient.Uri.ToString();
string? imageFileUrl = imageBlobClient.Uri.ToString();
var addUploadedFilesParameters = new { id, mp4FileUrl, imageFileUrl };
await sql.SaveData("spShorts_AddUploadedFiles", addUploadedFilesParameters, "sql");
return Results.Ok(new { Message = "Files uploaded to Azure Blob Storage successfully." });
});
app.Run();

View File

@ -0,0 +1,173 @@
{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_dependencyType": "compute.function.linux.appService"
},
"parameters": {
"resourceGroupName": {
"type": "string",
"defaultValue": "offline-demo-sql",
"metadata": {
"description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
}
},
"resourceGroupLocation": {
"type": "string",
"defaultValue": "southcentralus",
"metadata": {
"description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
}
},
"resourceName": {
"type": "string",
"defaultValue": "offlinedemoapi",
"metadata": {
"description": "Name of the main resource to be created by this template."
}
},
"resourceLocation": {
"type": "string",
"defaultValue": "[parameters('resourceGroupLocation')]",
"metadata": {
"description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
}
}
},
"resources": [
{
"type": "Microsoft.Resources/resourceGroups",
"name": "[parameters('resourceGroupName')]",
"location": "[parameters('resourceGroupLocation')]",
"apiVersion": "2019-10-01"
},
{
"type": "Microsoft.Resources/deployments",
"name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
"resourceGroup": "[parameters('resourceGroupName')]",
"apiVersion": "2019-10-01",
"dependsOn": [
"[parameters('resourceGroupName')]"
],
"properties": {
"mode": "Incremental",
"expressionEvaluationOptions": {
"scope": "inner"
},
"parameters": {
"resourceGroupName": {
"value": "[parameters('resourceGroupName')]"
},
"resourceGroupLocation": {
"value": "[parameters('resourceGroupLocation')]"
},
"resourceName": {
"value": "[parameters('resourceName')]"
},
"resourceLocation": {
"value": "[parameters('resourceLocation')]"
}
},
"template": {
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"resourceGroupName": {
"type": "string"
},
"resourceGroupLocation": {
"type": "string"
},
"resourceName": {
"type": "string"
},
"resourceLocation": {
"type": "string"
}
},
"variables": {
"storage_name": "[toLower(concat('storage', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId))))]",
"appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
"storage_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Storage/storageAccounts/', variables('storage_name'))]",
"appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]",
"function_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/sites/', parameters('resourceName'))]"
},
"resources": [
{
"location": "[parameters('resourceLocation')]",
"name": "[parameters('resourceName')]",
"type": "Microsoft.Web/sites",
"apiVersion": "2015-08-01",
"tags": {
"[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
},
"dependsOn": [
"[variables('appServicePlan_ResourceId')]",
"[variables('storage_ResourceId')]"
],
"kind": "functionapp",
"properties": {
"name": "[parameters('resourceName')]",
"kind": "functionapp",
"httpsOnly": true,
"reserved": false,
"serverFarmId": "[variables('appServicePlan_ResourceId')]",
"siteConfig": {
"alwaysOn": true,
"linuxFxVersion": "dotnet|3.1"
}
},
"identity": {
"type": "SystemAssigned"
},
"resources": [
{
"name": "appsettings",
"type": "config",
"apiVersion": "2015-08-01",
"dependsOn": [
"[variables('function_ResourceId')]"
],
"properties": {
"AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]",
"FUNCTIONS_EXTENSION_VERSION": "~3",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
}
}
]
},
{
"location": "[parameters('resourceGroupLocation')]",
"name": "[variables('storage_name')]",
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2017-10-01",
"tags": {
"[concat('hidden-related:', concat('/providers/Microsoft.Web/sites/', parameters('resourceName')))]": "empty"
},
"properties": {
"supportsHttpsTrafficOnly": true
},
"sku": {
"name": "Standard_LRS"
},
"kind": "Storage"
},
{
"location": "[parameters('resourceGroupLocation')]",
"name": "[variables('appServicePlan_name')]",
"type": "Microsoft.Web/serverFarms",
"apiVersion": "2015-02-01",
"kind": "linux",
"properties": {
"name": "[variables('appServicePlan_name')]",
"sku": "Standard",
"workerSizeId": "0",
"reserved": true
}
}
]
}
}
}
]
}

View File

@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5057",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7099;http://localhost:5057",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"AzureStorage": {
"ConnectionString": "",
"ContainerName": "shorts",
"StorageAccountName": "",
"StorageAccountKey": ""
},
"ConnectionStrings": {
"sql": ""
}
}

39
OfflineDemoApp.sln Normal file
View File

@ -0,0 +1,39 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34322.80
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OfflineDemo", "OfflineDemo\OfflineDemo.csproj", "{D519B7EB-0939-4288-ACFF-35901F8BF6A3}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OfflineDemoApi", "OfflineDemoApi\OfflineDemoApi.csproj", "{D79F7FAB-7778-4D3E-8F55-7C352B26E564}"
EndProject
Project("{00D1A9C2-B5F0-4AF3-8072-F6C62B433612}") = "OfflineDemoSql", "OfflineDemoSql\OfflineDemoSql.sqlproj", "{4E520D7D-BE86-466C-A70F-4B69BE5C698B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{D519B7EB-0939-4288-ACFF-35901F8BF6A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D519B7EB-0939-4288-ACFF-35901F8BF6A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D519B7EB-0939-4288-ACFF-35901F8BF6A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D519B7EB-0939-4288-ACFF-35901F8BF6A3}.Release|Any CPU.Build.0 = Release|Any CPU
{D79F7FAB-7778-4D3E-8F55-7C352B26E564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D79F7FAB-7778-4D3E-8F55-7C352B26E564}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D79F7FAB-7778-4D3E-8F55-7C352B26E564}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D79F7FAB-7778-4D3E-8F55-7C352B26E564}.Release|Any CPU.Build.0 = Release|Any CPU
{4E520D7D-BE86-466C-A70F-4B69BE5C698B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E520D7D-BE86-466C-A70F-4B69BE5C698B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E520D7D-BE86-466C-A70F-4B69BE5C698B}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{4E520D7D-BE86-466C-A70F-4B69BE5C698B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E520D7D-BE86-466C-A70F-4B69BE5C698B}.Release|Any CPU.Build.0 = Release|Any CPU
{4E520D7D-BE86-466C-A70F-4B69BE5C698B}.Release|Any CPU.Deploy.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {35585129-469B-4F71-AE66-BBC84B45E9D8}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<Name>OfflineDemoSql</Name>
<SchemaVersion>2.0</SchemaVersion>
<ProjectVersion>4.1</ProjectVersion>
<ProjectGuid>{4e520d7d-be86-466c-a70f-4b69be5c698b}</ProjectGuid>
<DSP>Microsoft.Data.Tools.Schema.Sql.SqlAzureV12DatabaseSchemaProvider</DSP>
<OutputType>Database</OutputType>
<RootPath>
</RootPath>
<RootNamespace>OfflineDemoSql</RootNamespace>
<AssemblyName>OfflineDemoSql</AssemblyName>
<ModelCollation>1033, CI</ModelCollation>
<DefaultFileStructure>BySchemaAndSchemaType</DefaultFileStructure>
<DeployToDatabase>True</DeployToDatabase>
<TargetFrameworkVersion>v4.7.2</TargetFrameworkVersion>
<TargetLanguage>CS</TargetLanguage>
<AppDesignerFolder>Properties</AppDesignerFolder>
<SqlServerVerification>False</SqlServerVerification>
<IncludeCompositeObjects>True</IncludeCompositeObjects>
<TargetDatabaseSet>True</TargetDatabaseSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\</OutputPath>
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
<TreatWarningsAsErrors>False</TreatWarningsAsErrors>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineDebug>false</DefineDebug>
<DefineTrace>true</DefineTrace>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<OutputPath>bin\Debug\</OutputPath>
<BuildScriptName>$(MSBuildProjectName).sql</BuildScriptName>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<DefineDebug>true</DefineDebug>
<DefineTrace>true</DefineTrace>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">11.0</VisualStudioVersion>
<!-- Default to the v11.0 targets path if the targets file for the current VS version is not found -->
<SSDTExists Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets')">True</SSDTExists>
<VisualStudioVersion Condition="'$(SSDTExists)' == ''">11.0</VisualStudioVersion>
</PropertyGroup>
<Import Condition="'$(SQLDBExtensionsRefPath)' != ''" Project="$(SQLDBExtensionsRefPath)\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<Import Condition="'$(SQLDBExtensionsRefPath)' == ''" Project="$(MSBuildExtensionsPath)\Microsoft\VisualStudio\v$(VisualStudioVersion)\SSDT\Microsoft.Data.Tools.Schema.SqlTasks.targets" />
<ItemGroup>
<Folder Include="Properties" />
</ItemGroup>
<ItemGroup>
<Build Include="Shorts.sql" />
<Build Include="spShorts_CreateNew.sql" />
<Build Include="spShorts_AddUploadedFiles.sql" />
<Build Include="spShorts_GetAll.sql" />
</ItemGroup>
</Project>

10
OfflineDemoSql/Shorts.sql Normal file
View File

@ -0,0 +1,10 @@
CREATE TABLE [dbo].[Shorts]
(
Id INT IDENTITY(1,1) PRIMARY KEY,
Title NVARCHAR(100) NOT NULL,
Description NVARCHAR(300),
Hashtags NVARCHAR(75),
Mp4FileUrl NVARCHAR(1024),
ImageFileUrl NVARCHAR(1024),
[IsUploaded] BIT NOT NULL DEFAULT 0
)

View File

@ -0,0 +1,10 @@
CREATE PROCEDURE [dbo].[spShorts_AddUploadedFiles]
@id int,
@mp4FileUrl nvarchar(1024),
@imageFileUrl nvarchar(1024)
AS
begin
update dbo.Shorts
set Mp4FileUrl = @mp4FileUrl, ImageFileUrl = @imageFileUrl
where Id = @id;
end

View File

@ -0,0 +1,11 @@
CREATE PROCEDURE [dbo].[spShorts_CreateNew]
@title nvarchar(100),
@description nvarchar(300),
@hashtags nvarchar(75)
AS
begin
insert into dbo.Shorts (Title, [Description], Hashtags)
values (@title, @description, @hashtags);
select cast(SCOPE_IDENTITY() as int);
end

View File

@ -0,0 +1,7 @@
CREATE PROCEDURE [dbo].[spShorts_GetAll]
AS
begin
select [Id], [Title], [Description], [Hashtags], [Mp4FileUrl], [ImageFileUrl], [IsUploaded]
from dbo.Shorts;
end