Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fe5f87c43 | ||
|
|
6ca1b92b0b | ||
|
|
6a085ba387 | ||
|
|
3c55c84f8d | ||
|
|
7fa0d97187 | ||
|
|
a592d42db1 | ||
|
|
6f4eefe600 | ||
|
|
6f667d96c2 | ||
|
|
8555aa5263 | ||
|
|
5ae8050b55 | ||
|
|
4e2308e9b5 | ||
|
|
cc71b347fb | ||
|
|
8478627095 | ||
|
|
a4715789a2 | ||
|
|
66403abc01 | ||
|
|
ad3e072c1c | ||
|
|
30c8f83c9f | ||
|
|
3c6a4f722e | ||
|
|
0f137755b5 | ||
|
|
1c9bc7fd5f | ||
|
|
d632cb75a7 | ||
|
|
68ab7f1696 | ||
|
|
c9380ec484 | ||
|
|
b8cb4a0116 | ||
|
|
f543365ab2 | ||
|
|
eb77dee91c | ||
|
|
23f823d96f | ||
|
|
ae4a1abdbe | ||
|
|
c998435312 | ||
|
|
9d58df9b89 | ||
|
|
4f0b299923 | ||
|
|
8719820f69 | ||
|
|
2e103cbdee | ||
|
|
8443f59c4e | ||
|
|
79ff66093f | ||
|
|
1190798093 | ||
|
|
3a245b695b | ||
|
|
2c529ae117 | ||
|
|
1396e85a3e | ||
|
|
fa8cc19750 | ||
|
|
d5f0a7601f | ||
|
|
f96b2c827c | ||
|
|
15da9dbc08 | ||
|
|
10253c7021 | ||
|
|
30eb4cd66d | ||
|
|
200d3ba26c | ||
|
|
1ece55d6a0 | ||
|
|
a63019fd2d | ||
|
|
5b6987227a | ||
|
|
6447e5eb79 | ||
|
|
5bd6de3875 | ||
|
|
a1dc295b68 | ||
|
|
dd390280fb | ||
|
|
ce510d8d21 | ||
|
|
d1b7867b37 | ||
|
|
8498414fb0 | ||
|
|
e2162300f0 | ||
|
|
d4512eef97 | ||
|
|
cb71b09aad | ||
|
|
d982c64aa7 | ||
|
|
5a541b78f1 | ||
|
|
7f8692ee29 | ||
|
|
716bb0dbce | ||
|
|
a5414ed424 | ||
|
|
67e0d0c8a5 | ||
|
|
85caccc899 | ||
|
|
060cccdcf9 | ||
|
|
abbd6ceb48 | ||
|
|
f02e6b8e5a | ||
|
|
a30f602b65 | ||
|
|
06e20737d0 | ||
|
|
bd335509ff | ||
|
|
6ad6662e01 | ||
|
|
a632cf3a2f | ||
|
|
0e09b8db39 | ||
|
|
04ef6d63bb | ||
|
|
aa5d1af299 | ||
|
|
c4ab287ba7 | ||
|
|
5a16c6f800 | ||
|
|
a3a74d23c4 | ||
|
|
6a05fadd45 | ||
|
|
e182597d8f | ||
|
|
4bc22a94eb | ||
|
|
a5afe90267 | ||
|
|
a3e1e62082 | ||
|
|
b90d24e5de | ||
|
|
39fe7b58a4 | ||
|
|
217a0294ac | ||
|
|
2c21d76245 | ||
|
|
83de634df1 | ||
|
|
7861eeeb07 | ||
|
|
9f5baf2aa1 | ||
|
|
8de5223dfc | ||
|
|
64200cc48f | ||
|
|
8ff4c8360f | ||
|
|
13fe2ec7ed | ||
|
|
57bae97ad7 | ||
|
|
c4df0914d9 | ||
|
|
9dd9fe9d9c | ||
|
|
75dbef8196 | ||
|
|
5f53228113 | ||
|
|
24114c1281 | ||
|
|
9702908b4d | ||
|
|
4e96d4f48e |
39
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
39
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug] - "
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Description:**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Expected Behaviour:**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Reproduction Steps:**
|
||||
Steps to reproduce the behaviour:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Cache Cleared:**
|
||||
If the issue is a front end issue please confirm you have first attempted to clear the cache and the issue still persists.
|
||||
|
||||
**Docker Compose:**
|
||||
If possible please provide your compose setup or a list of environment variables to help debug the issue.
|
||||
|
||||
**Device:**
|
||||
Pleases tell me some more details about the device you are having the issue on. Is the issue isolated to the device or can you reproduce on multiple devices?
|
||||
|
||||
- Desktop: [e.g. 1080p Chrome, 2K Safari, 4K Brave]
|
||||
- Mobile [e.g. iPhone 16, Samsung S24 Ultra]
|
||||
|
||||
**Screenshots:**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional Details:**
|
||||
Add any other context about the problem here.
|
||||
17
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature-request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Suggest an idea or enhancement for this project
|
||||
title: "[Feature] "
|
||||
labels: enhancement, feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Description:**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Examples:**
|
||||
A clear and concise description of any alternative solutions you've used that have similar functionality.
|
||||
|
||||
**Additional Details:**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
14
.github/ISSUE_TEMPLATE/translation-request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/translation-request.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Translation Request
|
||||
about: Create a report to help us improve
|
||||
title: "[Translations] - "
|
||||
labels: translations
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Description:**
|
||||
A clear and concise description of the changes being made.
|
||||
|
||||
**Pull Request:**
|
||||
A link to the pull request containing the updates to BOTH languages resource as specified by the documentation - `https://docs.wedding-share.org/docs/languages#addingupdating-languages`.
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -19,3 +19,8 @@
|
||||
/WeddingShare/wedding-share.db
|
||||
/WeddingShare/config/wedding-share.db
|
||||
/WeddingShare/wwwroot/thumbnails
|
||||
/WeddingShare/wwwroot/temp
|
||||
/WeddingShare/ffmpeg
|
||||
/WeddingShare/wwwroot/logos
|
||||
/WeddingShare/wwwroot/banners
|
||||
/WeddingShare/wwwroot/images/custom_resources
|
||||
|
||||
@@ -2,12 +2,14 @@ image: mcr.microsoft.com/dotnet/sdk:latest
|
||||
|
||||
stages:
|
||||
- test
|
||||
- build
|
||||
- push
|
||||
- release
|
||||
|
||||
variables:
|
||||
BUILD_DOCKERFILE: 'WeddingShare/Dockerfile'
|
||||
BUILD_CONFIGURATION: 'Release'
|
||||
BUILD_PLATFORMS: 'linux/arm/v7,linux/arm64/v8,linux/amd64'
|
||||
BUILD_BUILDER_NAME: 'wedding-share-builder'
|
||||
OBJECTS_DIRECTORY: 'obj'
|
||||
NUGET_PACKAGES_DIRECTORY: '.nuget'
|
||||
SOURCE_CODE_PATH: '*/*/'
|
||||
@@ -22,36 +24,25 @@ cache:
|
||||
|
||||
before_script:
|
||||
- 'docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY'
|
||||
# - 'docker buildx rm --all-inactive --force'
|
||||
# - 'docker buildx create --name $BUILD_BUILDER_NAME --driver=docker-container'
|
||||
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- 'dotnet restore'
|
||||
- 'dotnet test'
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- 'docker build -t $CI_REGISTRY_IMAGE:dev -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA -f WeddingShare/Dockerfile .'
|
||||
- 'docker push $CI_REGISTRY_IMAGE:dev'
|
||||
- 'docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA'
|
||||
needs:
|
||||
- test
|
||||
|
||||
push_pre_release:
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
stage: push
|
||||
only:
|
||||
- /^(prerel|rc|release)-[0-9]+\.[0-9]+\.[0-9]+$/
|
||||
- /^(prerel|rc|release)-[0-9]+.+/
|
||||
script:
|
||||
- 'docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA'
|
||||
- 'docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH'
|
||||
- 'docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH'
|
||||
- 'docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:pre_release'
|
||||
- 'docker push $CI_REGISTRY_IMAGE:pre_release'
|
||||
- 'docker buildx build --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH --tag $CI_REGISTRY_IMAGE:pre_release --platform $BUILD_PLATFORMS --builder $BUILD_BUILDER_NAME --push -f $BUILD_DOCKERFILE .'
|
||||
needs:
|
||||
- build
|
||||
- test
|
||||
|
||||
push_latest:
|
||||
variables:
|
||||
@@ -61,11 +52,9 @@ push_latest:
|
||||
- main
|
||||
- master
|
||||
script:
|
||||
- 'docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA'
|
||||
- 'docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:latest'
|
||||
- 'docker push $CI_REGISTRY_IMAGE:latest'
|
||||
- 'docker buildx build --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH --tag $CI_REGISTRY_IMAGE:latest --platform $BUILD_PLATFORMS --builder $BUILD_BUILDER_NAME --push -f $BUILD_DOCKERFILE .'
|
||||
needs:
|
||||
- build
|
||||
- test
|
||||
|
||||
push_tag:
|
||||
variables:
|
||||
@@ -74,24 +63,18 @@ push_tag:
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- 'docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA'
|
||||
- 'docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME'
|
||||
- 'docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME'
|
||||
- 'docker buildx build --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_NAME --platform $BUILD_PLATFORMS --builder $BUILD_BUILDER_NAME --push -f $BUILD_DOCKERFILE .'
|
||||
needs:
|
||||
- build
|
||||
- test
|
||||
|
||||
push_docker_hub:
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
stage: push
|
||||
stage: release
|
||||
only:
|
||||
- tags
|
||||
script:
|
||||
- 'docker pull $CI_REGISTRY_IMAGE:latest'
|
||||
- 'docker login -u $DOCKERHUB_USERNAME -p $DOCKERHUB_TOKEN'
|
||||
- 'docker tag $CI_REGISTRY_IMAGE:latest $DOCKERHUB_USERNAME/wedding_share:latest'
|
||||
- 'docker push $DOCKERHUB_USERNAME/wedding_share:latest'
|
||||
- 'docker tag $CI_REGISTRY_IMAGE:latest $DOCKERHUB_USERNAME/wedding_share:$CI_COMMIT_REF_NAME'
|
||||
- 'docker push $DOCKERHUB_USERNAME/wedding_share:$CI_COMMIT_REF_NAME'
|
||||
- 'docker buildx build --tag $DOCKERHUB_USERNAME/wedding_share:$CI_COMMIT_REF_NAME --tag $DOCKERHUB_USERNAME/wedding_share:latest --platform $BUILD_PLATFORMS --builder $BUILD_BUILDER_NAME --push -f $BUILD_DOCKERFILE .'
|
||||
needs:
|
||||
- build
|
||||
- push_tag
|
||||
23
WeddingShare.UnitTests/Helpers/JsonResponseHelper.cs
Normal file
23
WeddingShare.UnitTests/Helpers/JsonResponseHelper.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace WeddingShare.UnitTests.Helpers
|
||||
{
|
||||
internal class JsonResponseHelper
|
||||
{
|
||||
public static T GetPropertyValue<T>(object? obj, string propertyName, T defaultValue)
|
||||
{
|
||||
if (obj != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var val = obj?.GetType()?.GetProperty(propertyName)?.GetValue(obj, null);
|
||||
if (val != null)
|
||||
{
|
||||
return (T)val;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
74
WeddingShare.UnitTests/Helpers/MockData.cs
Normal file
74
WeddingShare.UnitTests/Helpers/MockData.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using WeddingShare.Enums;
|
||||
using WeddingShare.Models.Database;
|
||||
|
||||
namespace WeddingShare.UnitTests.Helpers
|
||||
{
|
||||
internal class MockData
|
||||
{
|
||||
public static DefaultHttpContext MockHttpContext(Dictionary<string, StringValues>? form = null, IFormFileCollection? files = null, MockSession session = null)
|
||||
{
|
||||
var ctx = new DefaultHttpContext()
|
||||
{
|
||||
Session = session ?? new MockSession()
|
||||
};
|
||||
|
||||
ctx.Request.Form = new FormCollection(form, files);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
public static List<GalleryItemModel> MockGalleryItems(int count = 10, int? galleryId = null, GalleryItemState state = GalleryItemState.All)
|
||||
{
|
||||
var result = new List<GalleryItemModel>();
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
result.Add(MockGalleryItem(galleryId, state));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static GalleryItemModel MockGalleryItem(int? galleryId = null, GalleryItemState state = GalleryItemState.All)
|
||||
{
|
||||
var rand = new Random();
|
||||
|
||||
return new GalleryItemModel()
|
||||
{
|
||||
Id = rand.Next(),
|
||||
GalleryId = galleryId != null ? (int)galleryId : rand.Next(),
|
||||
Title = $"{Guid.NewGuid()}.{MockFileExtension()}",
|
||||
UploadedBy = rand.Next(2) % 2 == 0 ? Guid.NewGuid().ToString() : null,
|
||||
MediaType = (MediaType)rand.Next(3),
|
||||
State = state == GalleryItemState.All ? (GalleryItemState)rand.Next(2) : state,
|
||||
FileSize = (int)rand.Next(2),
|
||||
};
|
||||
}
|
||||
|
||||
public static string MockFileExtension()
|
||||
{
|
||||
var rand = new Random();
|
||||
|
||||
string extension;
|
||||
switch (rand.Next(4))
|
||||
{
|
||||
case 0:
|
||||
extension = "jpg";
|
||||
break;
|
||||
case 1:
|
||||
extension = "jpeg";
|
||||
break;
|
||||
case 2:
|
||||
extension = "png";
|
||||
break;
|
||||
default:
|
||||
extension = "ffff";
|
||||
break;
|
||||
}
|
||||
|
||||
return rand.Next(2) % 2 == 0 ? extension.ToUpper() : extension.ToLower();
|
||||
}
|
||||
}
|
||||
}
|
||||
26
WeddingShare.UnitTests/Helpers/MockHttpMessageHandler.cs
Normal file
26
WeddingShare.UnitTests/Helpers/MockHttpMessageHandler.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace WeddingShare.UnitTests.Helpers
|
||||
{
|
||||
public class MockHttpMessageHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpStatusCode _statusCode;
|
||||
private readonly object? _responseContent;
|
||||
|
||||
public MockHttpMessageHandler(HttpStatusCode statusCode, object? responseContent = null)
|
||||
{
|
||||
_statusCode = statusCode;
|
||||
_responseContent = responseContent;
|
||||
}
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
return await Task.FromResult(new HttpResponseMessage
|
||||
{
|
||||
StatusCode = _statusCode,
|
||||
Content = JsonContent.Create(_responseContent)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
65
WeddingShare.UnitTests/Helpers/MockSession.cs
Normal file
65
WeddingShare.UnitTests/Helpers/MockSession.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace WeddingShare.UnitTests.Helpers
|
||||
{
|
||||
internal class MockSession : ISession
|
||||
{
|
||||
private IDictionary<string, string> Values = new Dictionary<string, string>();
|
||||
|
||||
public bool IsAvailable => throw new NotImplementedException();
|
||||
|
||||
public string Id => throw new NotImplementedException();
|
||||
|
||||
public IEnumerable<string> Keys => throw new NotImplementedException();
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
this.Values.Clear();
|
||||
}
|
||||
|
||||
public Task CommitAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Task LoadAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
this.Values.Remove(key);
|
||||
}
|
||||
|
||||
public void Set(string key, string value)
|
||||
{
|
||||
this.Set(key, Encoding.UTF8.GetBytes(value));
|
||||
}
|
||||
|
||||
public void Set(string key, byte[] value)
|
||||
{
|
||||
if (this.Values.ContainsKey(key))
|
||||
{
|
||||
this.Values.Remove(key);
|
||||
}
|
||||
|
||||
this.Values.Add(key, Encoding.UTF8.GetString(value));
|
||||
}
|
||||
|
||||
public bool TryGetValue(string key, [NotNullWhen(true)] out byte[]? value)
|
||||
{
|
||||
if (this.Values.ContainsKey(key))
|
||||
{
|
||||
value = Encoding.UTF8.GetBytes(this.Values[key]);
|
||||
return true;
|
||||
}
|
||||
|
||||
value = null;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Controllers;
|
||||
using WeddingShare.Enums;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Helpers.Notifications;
|
||||
using WeddingShare.Models;
|
||||
using WeddingShare.Models.Database;
|
||||
using WeddingShare.UnitTests.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class GalleryControllerTests
|
||||
{
|
||||
private readonly IWebHostEnvironment _env = Substitute.For<IWebHostEnvironment>();
|
||||
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
|
||||
private readonly IDatabaseHelper _database = Substitute.For<IDatabaseHelper>();
|
||||
private readonly IFileHelper _file = Substitute.For<IFileHelper>();
|
||||
private readonly IDeviceDetector _deviceDetector = Substitute.For<IDeviceDetector>();
|
||||
private readonly IImageHelper _image = Substitute.For<IImageHelper>();
|
||||
private readonly INotificationHelper _notification = Substitute.For<INotificationHelper>();
|
||||
private readonly IEncryptionHelper _encryption = Substitute.For<IEncryptionHelper>();
|
||||
private readonly WeddingShare.Helpers.IUrlHelper _url = Substitute.For<WeddingShare.Helpers.IUrlHelper>();
|
||||
private readonly ILogger<GalleryController> _logger = Substitute.For<ILogger<GalleryController>>();
|
||||
private readonly IStringLocalizer<Lang.Translations> _localizer = Substitute.For<IStringLocalizer<Lang.Translations>>();
|
||||
|
||||
public GalleryControllerTests()
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_env.WebRootPath.Returns("/app/wwwroot");
|
||||
|
||||
_database.GetGallery("default").Returns(Task.FromResult<GalleryModel?>(new GalleryModel()
|
||||
{
|
||||
Id = 1,
|
||||
Name = "default",
|
||||
SecretKey = "password",
|
||||
ApprovedItems = 32,
|
||||
PendingItems = 50,
|
||||
TotalItems = 72
|
||||
}));
|
||||
_database.GetGallery("blaa").Returns(Task.FromResult<GalleryModel?>(new GalleryModel()
|
||||
{
|
||||
Id = 2,
|
||||
Name = "blaa",
|
||||
SecretKey = "456789",
|
||||
ApprovedItems = 2,
|
||||
PendingItems = 1,
|
||||
TotalItems = 3
|
||||
}));
|
||||
_database.GetGallery("missing").Returns(Task.FromResult<GalleryModel?>(null));
|
||||
_database.AddGallery(Arg.Any<GalleryModel>()).Returns(Task.FromResult<GalleryModel?>(new GalleryModel()
|
||||
{
|
||||
Id = 101,
|
||||
Name = "missing",
|
||||
SecretKey = "123456",
|
||||
ApprovedItems = 0,
|
||||
PendingItems = 0,
|
||||
TotalItems = 0
|
||||
}));
|
||||
_database.AddGalleryItem(Arg.Any<GalleryItemModel>()).Returns(Task.FromResult<GalleryItemModel?>(MockData.MockGalleryItem()));
|
||||
|
||||
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.All, Arg.Any<MediaType>(), Arg.Any<ImageOrientation>(), Arg.Any<GalleryGroup>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.All)));
|
||||
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.Pending, Arg.Any<MediaType>(), Arg.Any<ImageOrientation>(), Arg.Any<GalleryGroup>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.Pending)));
|
||||
_database.GetAllGalleryItems(Arg.Any<int>(), GalleryItemState.Approved, Arg.Any<MediaType>(), Arg.Any<ImageOrientation>(), Arg.Any<GalleryGroup>(), Arg.Any<GalleryOrder>(), Arg.Any<int>(), Arg.Any<int>()).Returns(Task.FromResult(MockData.MockGalleryItems(10, 1, GalleryItemState.Approved)));
|
||||
_database.GetGalleryItemByChecksum(Arg.Any<int>(), Arg.Any<string>()).ReturnsNull();
|
||||
|
||||
_settings.GetOrDefault(Settings.Gallery.SecretKey, Arg.Any<string>(), Arg.Any<string>()).Returns("password");
|
||||
_settings.GetOrDefault(Settings.Gallery.SecretKey, Arg.Any<string>(), "blaa").Returns("456789");
|
||||
_settings.GetOrDefault(Settings.Gallery.SecretKey, Arg.Any<string>(), "missing").Returns("123456");
|
||||
_settings.GetOrDefault(Settings.Gallery.Upload, Arg.Any<bool>(), Arg.Any<string>()).Returns(true);
|
||||
_settings.GetOrDefault(Settings.Gallery.Download, Arg.Any<bool>(), Arg.Any<string>()).Returns(true);
|
||||
_settings.GetOrDefault(Settings.Gallery.UploadPeriod, Arg.Any<string>(), Arg.Any<string>()).Returns("1970-01-01 00:00:00");
|
||||
_settings.GetOrDefault(Settings.Gallery.PreventDuplicates, Arg.Any<bool>(), Arg.Any<string>()).Returns(true);
|
||||
_settings.GetOrDefault(Settings.Gallery.DefaultView, Arg.Any<int>(), Arg.Any<string>()).Returns((int)ViewMode.Default);
|
||||
_settings.GetOrDefault(Settings.Gallery.AllowedFileTypes, Arg.Any<string>(), Arg.Any<string>()).Returns(".jpg,.jpeg,.png,.mp4,.mov");
|
||||
_settings.GetOrDefault(Settings.Gallery.RequireReview, Arg.Any<bool>(), Arg.Any<string>()).Returns(true);
|
||||
_settings.GetOrDefault(Settings.Gallery.MaxFileSizeMB, Arg.Any<int>(), Arg.Any<string>()).Returns(10);
|
||||
|
||||
_file.GetChecksum(Arg.Any<string>()).Returns(Guid.NewGuid().ToString());
|
||||
|
||||
_notification.Send(Arg.Any<string>(), Arg.Any<string>()).Returns(Task.FromResult(true));
|
||||
|
||||
_localizer[Arg.Any<string>()].Returns(new LocalizedString("UnitTest", "UnitTest"));
|
||||
}
|
||||
|
||||
[TestCase(DeviceType.Desktop, 1, "default", "password", ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending, true)]
|
||||
[TestCase(DeviceType.Mobile, 2, "blaa", "456789", ViewMode.Presentation, GalleryGroup.Date, GalleryOrder.Ascending, true)]
|
||||
[TestCase(DeviceType.Tablet, 101, "missing", "123456", ViewMode.Slideshow, GalleryGroup.Uploader, GalleryOrder.Ascending, false)]
|
||||
public async Task GalleryController_Index(DeviceType deviceType, int id, string name, string? key, ViewMode? mode, GalleryGroup group, GalleryOrder order, bool existing)
|
||||
{
|
||||
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(deviceType);
|
||||
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(false);
|
||||
_settings.GetOrDefault(Settings.Basic.GuestGalleryCreation, Arg.Any<bool>()).Returns(false);
|
||||
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
|
||||
|
||||
if (existing)
|
||||
{
|
||||
ViewResult actual = (ViewResult)await controller.Index(name, key, mode, group, order);
|
||||
Assert.That(actual, Is.TypeOf<ViewResult>());
|
||||
Assert.That(actual?.Model, Is.Not.Null);
|
||||
|
||||
PhotoGallery model = (PhotoGallery)actual.Model;
|
||||
Assert.That(model?.GalleryId, Is.EqualTo(id));
|
||||
Assert.That(model?.GalleryName, Is.EqualTo(name));
|
||||
Assert.That(model.ViewMode, Is.EqualTo(mode));
|
||||
Assert.That(model?.FileUploader?.GalleryId, Is.EqualTo(name));
|
||||
Assert.That(model?.FileUploader?.SecretKey, Is.EqualTo(key));
|
||||
Assert.That(model?.FileUploader?.UploadUrl, Is.EqualTo("/Gallery/UploadImage"));
|
||||
}
|
||||
else
|
||||
{
|
||||
RedirectToActionResult actual = (RedirectToActionResult)await controller.Index(name, key, mode, group, order);
|
||||
Assert.That(actual, Is.TypeOf<RedirectToActionResult>());
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(true, true)]
|
||||
[TestCase(false, false)]
|
||||
public async Task GalleryController_UploadDisabled(bool disabled, bool expected)
|
||||
{
|
||||
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(DeviceType.Desktop);
|
||||
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(false);
|
||||
_settings.GetOrDefault(Settings.Gallery.Upload, Arg.Any<bool>(), Arg.Any<string>()).Returns(disabled);
|
||||
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
|
||||
|
||||
ViewResult actual = (ViewResult)await controller.Index("default", "password", ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending);
|
||||
Assert.That(actual, Is.TypeOf<ViewResult>());
|
||||
Assert.That(actual?.Model, Is.Not.Null);
|
||||
|
||||
PhotoGallery model = (PhotoGallery)actual.Model;
|
||||
Assert.That(model?.FileUploader, expected ? Is.Not.Null : Is.Null);
|
||||
}
|
||||
|
||||
[TestCase("1970-01-01 00:00", true)]
|
||||
[TestCase("3000-01-01 00:00", false)]
|
||||
[TestCase("1970-01-01 00:00 / 1980-01-01 00:00", false)]
|
||||
[TestCase("2999-01-01 00:00 / 3000-01-01 00:00", false)]
|
||||
[TestCase("1970-01-01 00:00 / 3000-01-01 00:00", true)]
|
||||
public async Task GalleryController_UploadDisabled(string uploadPeriod, bool expected)
|
||||
{
|
||||
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(DeviceType.Desktop);
|
||||
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(false);
|
||||
_settings.GetOrDefault(Settings.Gallery.UploadPeriod, Arg.Any<string>(), Arg.Any<string>()).Returns(uploadPeriod);
|
||||
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
|
||||
|
||||
ViewResult actual = (ViewResult)await controller.Index("default", "password", ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending);
|
||||
Assert.That(actual, Is.TypeOf<ViewResult>());
|
||||
Assert.That(actual?.Model, Is.Not.Null);
|
||||
|
||||
PhotoGallery model = (PhotoGallery)actual.Model;
|
||||
Assert.That(model?.FileUploader, expected ? Is.Not.Null : Is.Null);
|
||||
}
|
||||
|
||||
[TestCase(DeviceType.Desktop, ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending)]
|
||||
[TestCase(DeviceType.Mobile, ViewMode.Presentation, GalleryGroup.Date, GalleryOrder.Ascending)]
|
||||
[TestCase(DeviceType.Tablet, ViewMode.Slideshow, GalleryGroup.Uploader, GalleryOrder.Ascending)]
|
||||
public async Task GalleryController_Index_SingleGalleryMode(DeviceType deviceType, ViewMode? mode, GalleryGroup group, GalleryOrder order)
|
||||
{
|
||||
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(deviceType);
|
||||
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(true);
|
||||
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext();
|
||||
|
||||
ViewResult actual = (ViewResult)await controller.Index("default", "password", mode, group, order);
|
||||
Assert.That(actual, Is.TypeOf<ViewResult>());
|
||||
Assert.That(actual?.Model, Is.Not.Null);
|
||||
|
||||
PhotoGallery model = (PhotoGallery)actual.Model;
|
||||
Assert.That(model?.GalleryId, Is.EqualTo(1));
|
||||
Assert.That(model?.GalleryName, Is.EqualTo("default"));
|
||||
Assert.That(model.ViewMode, Is.EqualTo(mode));
|
||||
Assert.That(model?.FileUploader?.GalleryId, Is.EqualTo("default"));
|
||||
Assert.That(model?.FileUploader?.SecretKey, Is.EqualTo("password"));
|
||||
Assert.That(model?.FileUploader?.UploadUrl, Is.EqualTo("/Gallery/UploadImage"));
|
||||
}
|
||||
|
||||
[TestCase(true, 1, null)]
|
||||
[TestCase(true, 3, "Bob")]
|
||||
[TestCase(false, 1, "")]
|
||||
[TestCase(false, 3, "Unit Testing")]
|
||||
public async Task GalleryController_UploadImage(bool requiresReview, int fileCount, string? uploadedBy)
|
||||
{
|
||||
_settings.GetOrDefault(Settings.Gallery.RequireReview, Arg.Any<bool>()).Returns(requiresReview);
|
||||
|
||||
var files = new FormFileCollection();
|
||||
for (var i = 0; i < fileCount; i++)
|
||||
{
|
||||
files.Add(new FormFile(null, 0, 0, "TestFile_001", $"{Guid.NewGuid()}.jpg"));
|
||||
}
|
||||
|
||||
var session = new MockSession();
|
||||
session.Set(SessionKey.ViewerIdentity, uploadedBy ?? string.Empty);
|
||||
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext(
|
||||
session: session,
|
||||
form: new Dictionary<string, StringValues>
|
||||
{
|
||||
{ "Id", "default" },
|
||||
{ "SecretKey", "password" }
|
||||
},
|
||||
files: files);
|
||||
|
||||
JsonResult actual = (JsonResult)await controller.UploadImage();
|
||||
Assert.That(actual, Is.TypeOf<JsonResult>());
|
||||
Assert.That(actual?.Value, Is.Not.Null);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "success", false), Is.True);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(files.Count));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploadedBy", string.Empty), Is.EqualTo(!string.IsNullOrWhiteSpace(uploadedBy) ? uploadedBy : string.Empty));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.EqualTo(0));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public async Task GalleryController_UploadImage_Duplicate()
|
||||
{
|
||||
_database.GetGalleryItemByChecksum(Arg.Any<int>(), Arg.Any<string>()).Returns(Task.FromResult(MockData.MockGalleryItems(1, 1, GalleryItemState.Approved).FirstOrDefault()));
|
||||
|
||||
var files = new FormFileCollection();
|
||||
files.Add(new FormFile(null, 0, 0, "TestFile_001", $"{Guid.NewGuid()}.jpg"));
|
||||
|
||||
var session = new MockSession();
|
||||
session.Set(SessionKey.ViewerIdentity, string.Empty);
|
||||
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext(
|
||||
session: session,
|
||||
form: new Dictionary<string, StringValues>
|
||||
{
|
||||
{ "Id", "default" },
|
||||
{ "SecretKey", "password" }
|
||||
},
|
||||
files: files);
|
||||
|
||||
JsonResult actual = (JsonResult)await controller.UploadImage();
|
||||
Assert.That(actual, Is.TypeOf<JsonResult>());
|
||||
Assert.That(actual?.Value, Is.Not.Null);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "success", false), Is.False);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(0));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
public async Task GalleryController_UploadImage_InvalidGallery(string? id)
|
||||
{
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext(form: new Dictionary<string, StringValues>
|
||||
{
|
||||
{ "Id", id }
|
||||
});
|
||||
|
||||
JsonResult actual = (JsonResult)await controller.UploadImage();
|
||||
Assert.That(actual, Is.TypeOf<JsonResult>());
|
||||
Assert.That(actual?.Value, Is.Not.Null);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "success", false), Is.False);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(0));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[TestCase(null)]
|
||||
[TestCase("")]
|
||||
public async Task GalleryController_UploadImage_InvalidSecretKey(string? key)
|
||||
{
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext(form: new Dictionary<string, StringValues>
|
||||
{
|
||||
{ "Id", "default" },
|
||||
{ "SecretKey", key }
|
||||
});
|
||||
|
||||
JsonResult actual = (JsonResult)await controller.UploadImage();
|
||||
Assert.That(actual, Is.TypeOf<JsonResult>());
|
||||
Assert.That(actual?.Value, Is.Not.Null);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "success", false), Is.False);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(0));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task GalleryController_UploadImage_MissingGallery()
|
||||
{
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext(form: new Dictionary<string, StringValues>
|
||||
{
|
||||
{ "Id", Guid.NewGuid().ToString() }
|
||||
});
|
||||
|
||||
JsonResult actual = (JsonResult)await controller.UploadImage();
|
||||
Assert.That(actual, Is.TypeOf<JsonResult>());
|
||||
Assert.That(actual?.Value, Is.Not.Null);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "success", false), Is.False);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(0));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task GalleryController_UploadImage_NoFiles()
|
||||
{
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext(form: new Dictionary<string, StringValues>
|
||||
{
|
||||
{ "Id", "default" },
|
||||
{ "SecretKey", "password" }
|
||||
});
|
||||
|
||||
JsonResult actual = (JsonResult)await controller.UploadImage();
|
||||
Assert.That(actual, Is.TypeOf<JsonResult>());
|
||||
Assert.That(actual?.Value, Is.Not.Null);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "success", false), Is.False);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(0));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task GalleryController_UploadImage_FileTooBig()
|
||||
{
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext(
|
||||
form: new Dictionary<string, StringValues>
|
||||
{
|
||||
{ "Id", "default" },
|
||||
{ "SecretKey", "password" }
|
||||
},
|
||||
files: new FormFileCollection() {
|
||||
new FormFile(null, 0, int.MaxValue, "TestFile_001", $"{Guid.NewGuid()}.jpg")
|
||||
});
|
||||
|
||||
JsonResult actual = (JsonResult)await controller.UploadImage();
|
||||
Assert.That(actual, Is.TypeOf<JsonResult>());
|
||||
Assert.That(actual?.Value, Is.Not.Null);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "success", false), Is.False);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(0));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.GreaterThan(0));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task GalleryController_UploadImage_InvalidFileType()
|
||||
{
|
||||
var controller = new GalleryController(_env, _settings, _database, _file, _deviceDetector, _image, _notification, _encryption, _url, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = MockData.MockHttpContext(
|
||||
form: new Dictionary<string, StringValues>
|
||||
{
|
||||
{ "Id", "default" },
|
||||
{ "SecretKey", "password" }
|
||||
},
|
||||
files: new FormFileCollection() {
|
||||
new FormFile(null, 0, int.MaxValue, "TestFile_001", $"{Guid.NewGuid()}.blaa")
|
||||
});
|
||||
|
||||
JsonResult actual = (JsonResult)await controller.UploadImage();
|
||||
Assert.That(actual, Is.TypeOf<JsonResult>());
|
||||
Assert.That(actual?.Value, Is.Not.Null);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "success", false), Is.False);
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "uploaded", 0), Is.EqualTo(0));
|
||||
Assert.That(JsonResponseHelper.GetPropertyValue(actual.Value, "errors", new List<string>()).Count, Is.GreaterThan(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Controllers;
|
||||
using WeddingShare.Enums;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.UnitTests.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class HomeControllerTests
|
||||
{
|
||||
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
|
||||
private readonly IDatabaseHelper _database = Substitute.For<IDatabaseHelper>();
|
||||
private readonly IDeviceDetector _deviceDetector = Substitute.For<IDeviceDetector>();
|
||||
private readonly ILogger<HomeController> _logger = Substitute.For<ILogger<HomeController>>();
|
||||
private readonly IStringLocalizer<Lang.Translations> _localizer = Substitute.For<IStringLocalizer<Lang.Translations>>();
|
||||
|
||||
public HomeControllerTests()
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[TestCase(DeviceType.Desktop, true, "", true)]
|
||||
[TestCase(DeviceType.Desktop, false, "", false)]
|
||||
[TestCase(DeviceType.Mobile, true, "", true)]
|
||||
[TestCase(DeviceType.Mobile, false, "", false)]
|
||||
[TestCase(DeviceType.Desktop, true, "123456", false)]
|
||||
[TestCase(DeviceType.Desktop, false, "Abc123!", false)]
|
||||
[TestCase(DeviceType.Mobile, true, "abc123!", false)]
|
||||
[TestCase(DeviceType.Mobile, false, "adsbsds", false)]
|
||||
public async Task HomeController_Index(DeviceType deviceType, bool singleGalleryMode, string secretKey, bool isRedirect)
|
||||
{
|
||||
_deviceDetector.ParseDeviceType(Arg.Any<string>()).Returns(deviceType);
|
||||
_settings.GetOrDefault(Settings.Basic.SingleGalleryMode, Arg.Any<bool>()).Returns(singleGalleryMode);
|
||||
_settings.GetOrDefault(Settings.Gallery.SecretKey, Arg.Any<string>(), Arg.Any<string>()).Returns(secretKey);
|
||||
|
||||
var controller = new HomeController(_settings, _database, _deviceDetector, _logger, _localizer);
|
||||
controller.ControllerContext.HttpContext = new DefaultHttpContext()
|
||||
{
|
||||
Session = new MockSession()
|
||||
};
|
||||
|
||||
if (!isRedirect)
|
||||
{
|
||||
ViewResult actual = (ViewResult)await controller.Index();
|
||||
Assert.That(actual, Is.TypeOf<ViewResult>());
|
||||
}
|
||||
else
|
||||
{
|
||||
RedirectToActionResult actual = (RedirectToActionResult)await controller.Index();
|
||||
Assert.That(actual, Is.TypeOf<RedirectToActionResult>());
|
||||
Assert.That(actual.Permanent, Is.EqualTo(false));
|
||||
Assert.That(actual.ControllerName, Is.EqualTo("Gallery"));
|
||||
Assert.That(actual.ActionName, Is.EqualTo("Index"));
|
||||
Assert.That(actual.RouteValues, Is.Null);
|
||||
Assert.That(actual.Fragment, Is.Null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using WeddingShare.Extensions;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class DictionaryExtensionsTests
|
||||
{
|
||||
private readonly IDictionary<string, string> _data;
|
||||
|
||||
public DictionaryExtensionsTests()
|
||||
{
|
||||
_data = new Dictionary<string, string>()
|
||||
{
|
||||
{ "KEY_1", "1" },
|
||||
{ "KEY_2", "2" },
|
||||
{ "KEY_3", "3" },
|
||||
{ "KEY_4", "4" },
|
||||
};
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[TestCase("KEY_1", "1")]
|
||||
[TestCase("KEY_2", "2")]
|
||||
[TestCase("KEY_3", "3")]
|
||||
[TestCase("KEY_99", "")]
|
||||
public void DictionaryExtensions_GetValue(string key, string expected)
|
||||
{
|
||||
var actual = _data.GetValue(key);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("KEY_1", "Default", "1")]
|
||||
[TestCase("KEY_2", "Default", "2")]
|
||||
[TestCase("KEY_3", "Default", "3")]
|
||||
[TestCase("KEY_99", "Default", "Default")]
|
||||
public void DictionaryExtensions_GetValue_DefaultValue(string key, string defaultValue, string? expected)
|
||||
{
|
||||
var actual = _data.GetValue(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
_configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
|
||||
{
|
||||
{ "Release:Version", "v1.0.0" },
|
||||
{ "Release:Plugin:Version", "v3.0.0" },
|
||||
|
||||
{ "String1:Key1", "Value1" },
|
||||
{ "String1:Key2", "Value2" },
|
||||
@@ -57,108 +58,109 @@ namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
}
|
||||
|
||||
[TestCase("ENVKEY:1", "EnvValue1")]
|
||||
[TestCase("ENVKEY:2", "EnvValue2")]
|
||||
[TestCase("ENVKEY:3", "EnvValue3")]
|
||||
[TestCase("ENVKEY:4", null)]
|
||||
[TestCase("VERSION", "v2.0.0")]
|
||||
[TestCase("SETTINGS:ENVKEY:1", "EnvValue1")]
|
||||
[TestCase("SETTINGS:ENVKEY:2", "EnvValue2")]
|
||||
[TestCase("SETTINGS:ENVKEY:3", "EnvValue3")]
|
||||
[TestCase("SETTINGS:ENVKEY:4", null)]
|
||||
[TestCase("RELEASE:VERSION", null)]
|
||||
public void ConfigHelper_GetEnvironmentVariable(string section, string? expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetEnvironmentVariable(section);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("String1", "Key1", "Value1")]
|
||||
[TestCase("String1", "Key2", "Value2")]
|
||||
[TestCase("String2", "Key1", "Value3")]
|
||||
[TestCase("String2", "Key2", null)]
|
||||
[TestCase("Release", "Version", "v1.0.0")]
|
||||
public void ConfigHelper_GetConfigValue(string section, string key, string? expected)
|
||||
[TestCase("String1:Key1", "Value1")]
|
||||
[TestCase("String1:Key2", "Value2")]
|
||||
[TestCase("String2:Key1", "Value3")]
|
||||
[TestCase("String2:Key2", null)]
|
||||
[TestCase("Release:Version", "v1.0.0")]
|
||||
public void ConfigHelper_GetConfigValue(string key, string? expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetConfigValue(section, key);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetConfigValue(key);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("String1", "Key1", "Value1")]
|
||||
[TestCase("String1", "Key2", "Value2")]
|
||||
[TestCase("String2", "Key1", "Value3")]
|
||||
[TestCase("String2", "Key2", null)]
|
||||
[TestCase("Release", "Version", "v1.0.0")]
|
||||
public void ConfigHelper_Get(string section, string key, string? expected)
|
||||
[TestCase("String1:Key1", "Value1")]
|
||||
[TestCase("String1:Key2", "Value2")]
|
||||
[TestCase("String2:Key1", "Value3")]
|
||||
[TestCase("String2:Key2", null)]
|
||||
[TestCase("Release:Version", "v1.0.0")]
|
||||
public void ConfigHelper_Get(string key, string? expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).Get(section, key);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).Get(key);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("String1", "Key1", "Default", "Value1")]
|
||||
[TestCase("String1", "Key2", "Default", "Value2")]
|
||||
[TestCase("String2", "Key1", "Default", "Value3")]
|
||||
[TestCase("String2", "Key2", "Default", "Default")]
|
||||
[TestCase("Release", "Version", "v0.0.0", "v1.0.0")]
|
||||
public void ConfigHelper_GetOrDefault(string section, string key, string defaultValue, string expected)
|
||||
[TestCase("String1:Key1", "Default", "Value1")]
|
||||
[TestCase("String1:Key2", "Default", "Value2")]
|
||||
[TestCase("String2:Key1", "Default", "Value3")]
|
||||
[TestCase("String2:Key2", "Default", "Default")]
|
||||
[TestCase("Release:Version", "v0.0.0", "v1.0.0")]
|
||||
[TestCase("Release:Plugin:Version", "v0.0.0", "v3.0.0")]
|
||||
public void ConfigHelper_GetOrDefault(string key, string defaultValue, string expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(section, key, defaultValue);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Int1", "Key1", 999, 1)]
|
||||
[TestCase("Int1", "Key2", 999, 2)]
|
||||
[TestCase("Int2", "Key1", 999, 3)]
|
||||
[TestCase("Int2", "Key2", 999, 999)]
|
||||
public void ConfigHelper_GetOrDefault(string section, string key, int defaultValue, int expected)
|
||||
[TestCase("Int1:Key1", 999, 1)]
|
||||
[TestCase("Int1:Key2", 999, 2)]
|
||||
[TestCase("Int2:Key1", 999, 3)]
|
||||
[TestCase("Int2:Key2", 999, 999)]
|
||||
public void ConfigHelper_GetOrDefault(string key, int defaultValue, int expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(section, key, defaultValue);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Long1", "Key1", 999, 4)]
|
||||
[TestCase("Long1", "Key2", 999, 5)]
|
||||
[TestCase("Long2", "Key1", 999, 6)]
|
||||
[TestCase("Long2", "Key2", 999, 999)]
|
||||
public void ConfigHelper_GetOrDefault(string section, string key, long defaultValue, long expected)
|
||||
[TestCase("Long1:Key1", 999, 4)]
|
||||
[TestCase("Long1:Key2", 999, 5)]
|
||||
[TestCase("Long2:Key1", 999, 6)]
|
||||
[TestCase("Long2:Key2", 999, 999)]
|
||||
public void ConfigHelper_GetOrDefault(string key, long defaultValue, long expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(section, key, defaultValue);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Decimal1", "Key1", 999, 4.12)]
|
||||
[TestCase("Decimal1", "Key2", 999, 5.45)]
|
||||
[TestCase("Decimal2", "Key1", 999, 6.733)]
|
||||
[TestCase("Decimal2", "Key2", 999, 999)]
|
||||
public void ConfigHelper_GetOrDefault(string section, string key, decimal defaultValue, decimal expected)
|
||||
[TestCase("Decimal1:Key1", 999, 4.12)]
|
||||
[TestCase("Decimal1:Key2", 999, 5.45)]
|
||||
[TestCase("Decimal2:Key1", 999, 6.733)]
|
||||
[TestCase("Decimal2:Key2", 999, 999)]
|
||||
public void ConfigHelper_GetOrDefault(string key, decimal defaultValue, decimal expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(section, key, defaultValue);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Double1", "Key1", 999, 4.12)]
|
||||
[TestCase("Double1", "Key2", 999, 5.45)]
|
||||
[TestCase("Double2", "Key1", 999, 6.733)]
|
||||
[TestCase("Double2", "Key2", 999, 999)]
|
||||
public void ConfigHelper_GetOrDefault(string section, string key, double defaultValue, double expected)
|
||||
[TestCase("Double1:Key1", 999, 4.12)]
|
||||
[TestCase("Double1:Key2", 999, 5.45)]
|
||||
[TestCase("Double2:Key1", 999, 6.733)]
|
||||
[TestCase("Double2:Key2", 999, 999)]
|
||||
public void ConfigHelper_GetOrDefault(string key, double defaultValue, double expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(section, key, defaultValue);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Boolean1", "Key1", false, true)]
|
||||
[TestCase("Boolean1", "Key2", false, false)]
|
||||
[TestCase("Boolean2", "Key1", false, true)]
|
||||
[TestCase("Boolean2", "Key2", true, true)]
|
||||
public void ConfigHelper_GetOrDefault(string section, string key, bool defaultValue, bool expected)
|
||||
[TestCase("Boolean1:Key1", false, true)]
|
||||
[TestCase("Boolean1:Key2", false, false)]
|
||||
[TestCase("Boolean2:Key1", false, true)]
|
||||
[TestCase("Boolean2:Key2", true, true)]
|
||||
public void ConfigHelper_GetOrDefault(string key, bool defaultValue, bool expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(section, key, defaultValue);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("DateTime1", "Key1", null, "1987-11-20 08:00:00")]
|
||||
[TestCase("DateTime1", "Key2", null, "2000-08-12 12:00:00")]
|
||||
[TestCase("DateTime2", "Key1", null, "2018-01-01 20:30:10")]
|
||||
[TestCase("DateTime2", "Key2", null, null)]
|
||||
[TestCase("DateTime3", "Key3", "2350-05-05 00:05:12", "2350-05-05 00:05:12")]
|
||||
public void ConfigHelper_GetOrDefault(string section, string key, DateTime? defaultValue, string? expected)
|
||||
[TestCase("DateTime1:Key1", null, "1987-11-20 08:00:00")]
|
||||
[TestCase("DateTime1:Key2", null, "2000-08-12 12:00:00")]
|
||||
[TestCase("DateTime2:Key1", null, "2018-01-01 20:30:10")]
|
||||
[TestCase("DateTime2:Key2", null, null)]
|
||||
[TestCase("DateTime3:Key3", "2350-05-05 00:05:12", "2350-05-05 00:05:12")]
|
||||
public void ConfigHelper_GetOrDefault(string key, DateTime? defaultValue, string? expected)
|
||||
{
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(section, key, defaultValue);
|
||||
var actual = new ConfigHelper(_environment, _configuration, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(!string.IsNullOrWhiteSpace(expected) ? DateTime.Parse(expected) : null));
|
||||
}
|
||||
}
|
||||
|
||||
108
WeddingShare.UnitTests/Tests/Helpers/EmailHelper.cs
Normal file
108
WeddingShare.UnitTests/Tests/Helpers/EmailHelper.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Net.Mail;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NSubstitute;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Notifications;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class EmailHelperTests
|
||||
{
|
||||
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
|
||||
private readonly ISmtpClientWrapper _smtp = Substitute.For<ISmtpClientWrapper>();
|
||||
private readonly ILogger<EmailHelper> _logger = Substitute.For<ILogger<EmailHelper>>();
|
||||
|
||||
public EmailHelperTests()
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_smtp.SendMailAsync(Arg.Any<SmtpClient>(), Arg.Any<MailMessage>()).Returns(Task.FromResult(true));
|
||||
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, Arg.Any<bool>()).Returns(true);
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Recipient, Arg.Any<string>()).Returns("unit@test.com");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Host, Arg.Any<string>()).Returns("https://unit.test.com/");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Port, Arg.Any<int>()).Returns(999);
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Username, Arg.Any<string>()).Returns("Unit");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Password, Arg.Any<string>()).Returns("Test");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.From, Arg.Any<string>()).Returns("unittest@test.com");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, Arg.Any<string>()).Returns("UnitTest");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.UseSSL, Arg.Any<bool>()).Returns(true);
|
||||
}
|
||||
|
||||
[TestCase("unit", "test")]
|
||||
public async Task EmailHelper_Success(string title, string message)
|
||||
{
|
||||
var actual = await new EmailHelper(_settings, _smtp, _logger).Send(title, message);
|
||||
Assert.That(actual, Is.EqualTo(true));
|
||||
}
|
||||
|
||||
[TestCase(true, true)]
|
||||
[TestCase(false, false)]
|
||||
public async Task EmailHelper_Enabled(bool enabled, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, Arg.Any<bool>()).Returns(enabled);
|
||||
|
||||
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, false)]
|
||||
[TestCase("", false)]
|
||||
[TestCase("blaa@blaa.com", true)]
|
||||
public async Task EmailHelper_Recipient(string recipient, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Recipient, Arg.Any<string>()).Returns(recipient);
|
||||
|
||||
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, false)]
|
||||
[TestCase("", false)]
|
||||
[TestCase("https://unit.test.com/", true)]
|
||||
public async Task EmailHelper_Host(string host, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Host, Arg.Any<string>()).Returns(host);
|
||||
|
||||
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(-100, false)]
|
||||
[TestCase(-1, false)]
|
||||
[TestCase(0, false)]
|
||||
[TestCase(1, true)]
|
||||
public async Task EmailHelper_Port(int port, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Port, Arg.Any<int>()).Returns(port);
|
||||
|
||||
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, false)]
|
||||
[TestCase("", false)]
|
||||
[TestCase("blaa@blaa.com", true)]
|
||||
public async Task EmailHelper_From(string from, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.From, Arg.Any<string>()).Returns(from);
|
||||
|
||||
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, true)]
|
||||
[TestCase("", true)]
|
||||
[TestCase("UnitTest", true)]
|
||||
public async Task EmailHelper_DisplayName(string displayName, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, Arg.Any<string>()).Returns(displayName);
|
||||
|
||||
var actual = await new EmailHelper(_settings, _smtp, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
86
WeddingShare.UnitTests/Tests/Helpers/EncrytpionHelper.cs
Normal file
86
WeddingShare.UnitTests/Tests/Helpers/EncrytpionHelper.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class EncrytpionHelper
|
||||
{
|
||||
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
|
||||
|
||||
public EncrytpionHelper()
|
||||
{
|
||||
_settings.GetOrDefault(Security.Encryption.HashType, Arg.Any<string>()).Returns("SHA256");
|
||||
_settings.GetOrDefault(Security.Encryption.Iterations, Arg.Any<int>()).Returns(1000);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[TestCase("Test", "Key1", "Salt1", "ZMw15YpZ+uph9psdR6tEZg==")]
|
||||
[TestCase("Test", "Key2", "Salt2", "p/fwjLVXvJ2dRKbXDNhxDA==")]
|
||||
[TestCase("Test", "Key3", "Salt3", "47VYeotX2C8GPuhaQlrWXg==")]
|
||||
public void EncrytpionHelper_ValidDetails(string value, string key, string salt, string expected)
|
||||
{
|
||||
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(key);
|
||||
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(salt);
|
||||
|
||||
var actual = new EncryptionHelper(_settings).Encrypt(value);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Test1", "Salt1")]
|
||||
[TestCase("Test2", "Salt2")]
|
||||
[TestCase("Test3", "Salt3")]
|
||||
public void EncrytpionHelper_NoKey(string value, string salt)
|
||||
{
|
||||
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(string.Empty);
|
||||
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(salt);
|
||||
|
||||
var actual = new EncryptionHelper(_settings).Encrypt(value);
|
||||
Assert.That(actual, Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[TestCase("Test1", "Key1")]
|
||||
[TestCase("Test2", "Key2")]
|
||||
[TestCase("Test3", "Key3")]
|
||||
public void EncrytpionHelper_NoSalt(string value, string key)
|
||||
{
|
||||
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(key);
|
||||
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(string.Empty);
|
||||
|
||||
var actual = new EncryptionHelper(_settings).Encrypt(value);
|
||||
Assert.That(actual, Is.EqualTo(value));
|
||||
}
|
||||
|
||||
[TestCase("Test1", "Key1", "Salt1")]
|
||||
[TestCase("Test2", "Key2", "Salt2")]
|
||||
[TestCase("Test3", "Key3", "Salt3")]
|
||||
public void EncrytpionHelper_DifferentHashes(string value, string key, string salt)
|
||||
{
|
||||
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(key);
|
||||
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(salt);
|
||||
var helper1 = new EncryptionHelper(_settings).Encrypt(value);
|
||||
|
||||
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns("Unit");
|
||||
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns("Test");
|
||||
var helper2 = new EncryptionHelper(_settings).Encrypt(value);
|
||||
|
||||
Assert.That(helper1, Is.Not.EqualTo(helper2));
|
||||
}
|
||||
|
||||
[TestCase("Key", "", false)]
|
||||
[TestCase("", "Salt", false)]
|
||||
[TestCase("Key", "Salt", true)]
|
||||
public void EncrytpionHelper_IsEncryptionEnabled(string key, string salt, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Security.Encryption.Key, Arg.Any<string>()).Returns(key);
|
||||
_settings.GetOrDefault(Security.Encryption.Salt, Arg.Any<string>()).Returns(salt);
|
||||
|
||||
var actual = new EncryptionHelper(_settings).IsEncryptionEnabled();
|
||||
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
81
WeddingShare.UnitTests/Tests/Helpers/FileHelper.cs
Normal file
81
WeddingShare.UnitTests/Tests/Helpers/FileHelper.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WeddingShare.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class FileHelperTests
|
||||
{
|
||||
private readonly ILogger<FileHelper> _logger = Substitute.For<ILogger<FileHelper>>();
|
||||
|
||||
public FileHelperTests()
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[TestCase(-1, "0 B")]
|
||||
[TestCase(0, "0 B")]
|
||||
[TestCase(1, "1 B")]
|
||||
[TestCase(2, "2 B")]
|
||||
[TestCase(3, "3 B")]
|
||||
public void FileHelper_BytesToHumanReadable(long bytes, string expected)
|
||||
{
|
||||
var actual = new FileHelper(_logger).BytesToHumanReadable(bytes, 0);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(-1, "0 B")]
|
||||
[TestCase(0, "0 B")]
|
||||
[TestCase(1, "1 B")]
|
||||
[TestCase(10, "10 B")]
|
||||
[TestCase(100, "100 B")]
|
||||
[TestCase(1000, "1 KB")]
|
||||
[TestCase(1000000, "1 MB")]
|
||||
[TestCase(1000000000, "1 GB")]
|
||||
[TestCase(1000000000000, "1 TB")]
|
||||
[TestCase(1000000000000000, "1 PB")]
|
||||
[TestCase(1000000000000000000, "1 EB")]
|
||||
public void FileHelper_BytesToHumanReadable_No_Places(long bytes, string expected)
|
||||
{
|
||||
var actual = new FileHelper(_logger).BytesToHumanReadable(bytes, 0);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(-1, "0.0 B")]
|
||||
[TestCase(0, "0.0 B")]
|
||||
[TestCase(1, "1.0 B")]
|
||||
[TestCase(10, "10.0 B")]
|
||||
[TestCase(100, "100.0 B")]
|
||||
[TestCase(1000, "1.0 KB")]
|
||||
[TestCase(1000000, "1.0 MB")]
|
||||
[TestCase(1000000000, "1.0 GB")]
|
||||
[TestCase(1000000000000, "1.0 TB")]
|
||||
[TestCase(1000000000000000, "1.0 PB")]
|
||||
[TestCase(1000000000000000000, "1.0 EB")]
|
||||
public void FileHelper_BytesToHumanReadable_1_Places(long bytes, string expected)
|
||||
{
|
||||
var actual = new FileHelper(_logger).BytesToHumanReadable(bytes, 1);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(-1, "0.00 B")]
|
||||
[TestCase(0, "0.00 B")]
|
||||
[TestCase(1, "1.00 B")]
|
||||
[TestCase(10, "10.00 B")]
|
||||
[TestCase(100, "100.00 B")]
|
||||
[TestCase(1000, "1.00 KB")]
|
||||
[TestCase(1000000, "1.00 MB")]
|
||||
[TestCase(1000000000, "1.00 GB")]
|
||||
[TestCase(1000000000000, "1.00 TB")]
|
||||
[TestCase(1000000000000000, "1.00 PB")]
|
||||
[TestCase(1000000000000000000, "1.00 EB")]
|
||||
public void FileHelper_BytesToHumanReadable_2_Places(long bytes, string expected)
|
||||
{
|
||||
var actual = new FileHelper(_logger).BytesToHumanReadable(bytes, 2);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
88
WeddingShare.UnitTests/Tests/Helpers/GotifyHelper.cs
Normal file
88
WeddingShare.UnitTests/Tests/Helpers/GotifyHelper.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Notifications;
|
||||
using WeddingShare.UnitTests.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class GotifyHelperTests
|
||||
{
|
||||
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
|
||||
private readonly IHttpClientFactory _clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
private readonly ILogger<GotifyHelper> _logger = Substitute.For<ILogger<GotifyHelper>>();
|
||||
|
||||
public GotifyHelperTests()
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var client = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.OK));
|
||||
client.BaseAddress = new Uri("https://unit.test.com/");
|
||||
|
||||
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, Arg.Any<bool>()).Returns(true);
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, Arg.Any<string>()).Returns("https://unit.test.com/");
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Token, Arg.Any<string>()).Returns("UnitTest");
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Priority, Arg.Any<int>()).Returns(4);
|
||||
}
|
||||
|
||||
[TestCase("unit", "test")]
|
||||
public async Task GotifyHelper_Success(string title, string message)
|
||||
{
|
||||
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send(title, message);
|
||||
Assert.That(actual, Is.EqualTo(true));
|
||||
}
|
||||
|
||||
[TestCase(true, true)]
|
||||
[TestCase(false, false)]
|
||||
public async Task GotifyHelper_Enabled(bool enabled, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, Arg.Any<bool>()).Returns(enabled);
|
||||
|
||||
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, false)]
|
||||
[TestCase("http://unittest.com", true)]
|
||||
[TestCase("https://unittest.com", true)]
|
||||
public async Task GotifyHelper_Endpoint(string? endpoint, bool expected)
|
||||
{
|
||||
var client = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.OK));
|
||||
client.BaseAddress = endpoint != null ? new Uri(endpoint) : null;
|
||||
|
||||
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, false)]
|
||||
[TestCase("", false)]
|
||||
[TestCase("UnitTest", true)]
|
||||
public async Task GotifyHelper_Token(string? token, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Token, Arg.Any<string>()).Returns(token);
|
||||
|
||||
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(-100, false)]
|
||||
[TestCase(-1, false)]
|
||||
[TestCase(0, false)]
|
||||
[TestCase(1, true)]
|
||||
[TestCase(100, true)]
|
||||
public async Task GotifyHelper_Priority(int priority, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Priority, Arg.Any<int>()).Returns(priority);
|
||||
|
||||
var actual = await new GotifyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.Extensions.Localization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
@@ -8,7 +9,9 @@ namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class ImageHelperTests
|
||||
{
|
||||
private readonly IFileHelper _fileHelper = Substitute.For<IFileHelper>();
|
||||
private readonly ILogger<ImageHelper> _logger = Substitute.For<ILogger<ImageHelper>>();
|
||||
private readonly IStringLocalizer<Lang.Translations> _localizer = Substitute.For<IStringLocalizer<Lang.Translations>>();
|
||||
private readonly IDictionary<ImageOrientation, Image?> _imageCollection;
|
||||
|
||||
public ImageHelperTests()
|
||||
@@ -34,7 +37,7 @@ namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
var image = _imageCollection[orientation];
|
||||
Assert.IsNotNull(image);
|
||||
|
||||
var actual = new ImageHelper(_logger).GetOrientation(image);
|
||||
var actual = new ImageHelper(_fileHelper, _logger, _localizer).GetOrientation(image);
|
||||
Assert.That(actual, Is.EqualTo(orientation));
|
||||
}
|
||||
}
|
||||
|
||||
95
WeddingShare.UnitTests/Tests/Helpers/NotificationBroker.cs
Normal file
95
WeddingShare.UnitTests/Tests/Helpers/NotificationBroker.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Notifications;
|
||||
using WeddingShare.UnitTests.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class NotificationBrokerTests
|
||||
{
|
||||
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
|
||||
private readonly IHttpClientFactory _clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
private readonly ISmtpClientWrapper _smtp = Substitute.For<ISmtpClientWrapper>();
|
||||
private readonly ILoggerFactory _logger = Substitute.For<ILoggerFactory>();
|
||||
|
||||
public NotificationBrokerTests()
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var client = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.OK));
|
||||
client.BaseAddress = new Uri("https://unit.test.com/");
|
||||
|
||||
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
_smtp.SendMailAsync(Arg.Any<SmtpClient>(), Arg.Any<MailMessage>()).Returns(Task.FromResult(true));
|
||||
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, Arg.Any<bool>()).Returns(true);
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Recipient, Arg.Any<string>()).Returns("unit@test.com");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Host, Arg.Any<string>()).Returns("https://unit.test.com/");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Port, Arg.Any<int>()).Returns(999);
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Username, Arg.Any<string>()).Returns("Unit");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Password, Arg.Any<string>()).Returns("Test");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.From, Arg.Any<string>()).Returns("unittest@test.com");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, Arg.Any<string>()).Returns("UnitTest");
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.UseSSL, Arg.Any<bool>()).Returns(true);
|
||||
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, Arg.Any<bool>()).Returns(true);
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, Arg.Any<string>()).Returns("https://unit.test.com/");
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Token, Arg.Any<string>()).Returns("UnitTest");
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Topic, Arg.Any<string>()).Returns("UnitTest");
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Priority, Arg.Any<int>()).Returns(4);
|
||||
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, Arg.Any<bool>()).Returns(true);
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, Arg.Any<string>()).Returns("https://unit.test.com/");
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Token, Arg.Any<string>()).Returns("UnitTest");
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Priority, Arg.Any<int>()).Returns(4);
|
||||
}
|
||||
|
||||
[TestCase(false, false, false, true)]
|
||||
[TestCase(true, false, false, true)]
|
||||
[TestCase(false, true, false, true)]
|
||||
[TestCase(false, false, true, true)]
|
||||
[TestCase(true, true, true, true)]
|
||||
public async Task NotificationBroker_Success(bool smtp, bool ntfy, bool gotify, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, Arg.Any<bool>()).Returns(smtp);
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, Arg.Any<bool>()).Returns(ntfy);
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, Arg.Any<bool>()).Returns(gotify);
|
||||
|
||||
var actual = await new NotificationBroker(_settings, _smtp, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task NotificationBroker_Issue_Smtp()
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Smtp.Host, Arg.Any<string>()).Returns(string.Empty);
|
||||
|
||||
var actual = await new NotificationBroker(_settings, _smtp, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(false));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task NotificationBroker_Issue_Ntfy()
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, Arg.Any<string>()).Returns(string.Empty);
|
||||
|
||||
var actual = await new NotificationBroker(_settings, _smtp, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(false));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task NotificationBroker_Issue_Gotify()
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, Arg.Any<string>()).Returns(string.Empty);
|
||||
|
||||
var actual = await new NotificationBroker(_settings, _smtp, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
101
WeddingShare.UnitTests/Tests/Helpers/NtfyHelper.cs
Normal file
101
WeddingShare.UnitTests/Tests/Helpers/NtfyHelper.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Notifications;
|
||||
using WeddingShare.UnitTests.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class NtfyHelperTests
|
||||
{
|
||||
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
|
||||
private readonly IHttpClientFactory _clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
private readonly ILogger<NtfyHelper> _logger = Substitute.For<ILogger<NtfyHelper>>();
|
||||
|
||||
public NtfyHelperTests()
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var client = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.OK));
|
||||
client.BaseAddress = new Uri("https://unit.test.com/");
|
||||
|
||||
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, Arg.Any<bool>()).Returns(true);
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, Arg.Any<string>()).Returns("https://unit.test.com/");
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Token, Arg.Any<string>()).Returns("UnitTest");
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Topic, Arg.Any<string>()).Returns("UnitTest");
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Priority, Arg.Any<int>()).Returns(4);
|
||||
}
|
||||
|
||||
[TestCase("unit", "test")]
|
||||
public async Task NtfyHelper_Success(string title, string message)
|
||||
{
|
||||
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send(title, message);
|
||||
Assert.That(actual, Is.EqualTo(true));
|
||||
}
|
||||
|
||||
[TestCase(true, true)]
|
||||
[TestCase(false, false)]
|
||||
public async Task NtfyHelper_Enabled(bool enabled, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, Arg.Any<bool>()).Returns(enabled);
|
||||
|
||||
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, false)]
|
||||
[TestCase("http://unittest.com", true)]
|
||||
[TestCase("https://unittest.com", true)]
|
||||
public async Task NtfyHelper_Endpoint(string? endpoint, bool expected)
|
||||
{
|
||||
var client = new HttpClient(new MockHttpMessageHandler(HttpStatusCode.OK));
|
||||
client.BaseAddress = endpoint != null ? new Uri(endpoint) : null;
|
||||
|
||||
_clientFactory.CreateClient(Arg.Any<string>()).Returns(client);
|
||||
|
||||
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, false)]
|
||||
[TestCase("", false)]
|
||||
[TestCase("UnitTest", true)]
|
||||
public async Task NtfyHelper_Token(string? token, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Token, Arg.Any<string>()).Returns(token);
|
||||
|
||||
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, false)]
|
||||
[TestCase("", false)]
|
||||
[TestCase("UnitTest", true)]
|
||||
public async Task NtfyHelper_Topic(string? topic, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Topic, Arg.Any<string>()).Returns(topic);
|
||||
|
||||
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(-100, false)]
|
||||
[TestCase(-1, false)]
|
||||
[TestCase(0, false)]
|
||||
[TestCase(1, true)]
|
||||
[TestCase(100, true)]
|
||||
public async Task NtfyHelper_Priority(int priority, bool expected)
|
||||
{
|
||||
_settings.GetOrDefault(Constants.Notifications.Ntfy.Priority, Arg.Any<int>()).Returns(priority);
|
||||
|
||||
var actual = await new NtfyHelper(_settings, _clientFactory, _logger).Send("unit", "test");
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Models.Database;
|
||||
using WeddingShare.UnitTests.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class SecretKeyHelperTests
|
||||
{
|
||||
private readonly IDatabaseHelper _database = Substitute.For<IDatabaseHelper>();
|
||||
|
||||
public SecretKeyHelperTests()
|
||||
{
|
||||
_database.GetGallery("Gallery1").Returns(new GalleryModel() { SecretKey = "001" });
|
||||
_database.GetGallery("Gallery2").Returns(new GalleryModel() { SecretKey = "002" });
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task SecretKeyHelper_GetGallerySecretKey_DefaultEnvKey()
|
||||
{
|
||||
var environment = Substitute.For<IEnvironmentWrapper>();
|
||||
environment.GetEnvironmentVariable("SECRET_KEY").Returns("123");
|
||||
|
||||
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
|
||||
{
|
||||
{ "Secret_Key_Gallery2", "002" }
|
||||
});
|
||||
|
||||
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
|
||||
|
||||
var actual = await new SecretKeyHelper(config, _database).GetGallerySecretKey("Gallery3");
|
||||
Assert.That(actual, Is.EqualTo("123"));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task SecretKeyHelper_GetGallerySecretKey_GalleryEnvKey()
|
||||
{
|
||||
var environment = Substitute.For<IEnvironmentWrapper>();
|
||||
environment.GetEnvironmentVariable("SECRET_KEY").Returns("123");
|
||||
environment.GetEnvironmentVariable("SECRET_KEY_GALLERY1").Returns("001");
|
||||
|
||||
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
|
||||
{
|
||||
{ "Secret_Key_Gallery2", "002" }
|
||||
});
|
||||
|
||||
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
|
||||
|
||||
var actual = await new SecretKeyHelper(config, _database).GetGallerySecretKey("Gallery1");
|
||||
Assert.That(actual, Is.EqualTo("001"));
|
||||
}
|
||||
|
||||
[TestCase("Gallery1", "001")]
|
||||
[TestCase("Gallery2", "002")]
|
||||
[TestCase("Gallery3", null)]
|
||||
public async Task SecretKeyHelper_GetGallerySecretKey_Database(string galleryId, string key)
|
||||
{
|
||||
var environment = Substitute.For<IEnvironmentWrapper>();
|
||||
environment.GetEnvironmentVariable(Arg.Any<string>()).Returns(string.Empty);
|
||||
|
||||
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>());
|
||||
|
||||
var config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
|
||||
|
||||
var actual = await new SecretKeyHelper(config, _database).GetGallerySecretKey(galleryId);
|
||||
Assert.That(actual, Is.EqualTo(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
175
WeddingShare.UnitTests/Tests/Helpers/SettingsHelper.cs
Normal file
175
WeddingShare.UnitTests/Tests/Helpers/SettingsHelper.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Models.Database;
|
||||
using WeddingShare.UnitTests.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class SettingsHelperTests
|
||||
{
|
||||
private readonly IConfigHelper _config;
|
||||
private readonly IDatabaseHelper _database = Substitute.For<IDatabaseHelper>();
|
||||
private readonly ILogger<SettingsHelper> _logger = Substitute.For<ILogger<SettingsHelper>>();
|
||||
|
||||
public SettingsHelperTests()
|
||||
{
|
||||
var environment = Substitute.For<IEnvironmentWrapper>();
|
||||
environment.GetEnvironmentVariable("VERSION").Returns("v2.0.0");
|
||||
environment.GetEnvironmentVariable("ENVKEY_1").Returns("EnvValue1");
|
||||
environment.GetEnvironmentVariable("ENVKEY_2").Returns("EnvValue2");
|
||||
environment.GetEnvironmentVariable("ENVKEY_3").Returns("EnvValue3");
|
||||
|
||||
var configuration = ConfigurationHelper.MockConfiguration(new Dictionary<string, string?>()
|
||||
{
|
||||
{ "Release:Version", "v1.0.0" },
|
||||
{ "Release:Plugin:Version", "v3.0.0" },
|
||||
|
||||
{ "String1:Key1", "Value1" },
|
||||
{ "String1:Key2", "Value2" },
|
||||
{ "String2:Key1", "Value3" },
|
||||
|
||||
{ "Int1:Key1", "1" },
|
||||
{ "Int1:Key2", "2" },
|
||||
{ "Int2:Key1", "3" },
|
||||
|
||||
{ "Long1:Key1", "4" },
|
||||
{ "Long1:Key2", "5" },
|
||||
{ "Long2:Key1", "6" },
|
||||
|
||||
{ "Decimal1:Key1", "4.12" },
|
||||
{ "Decimal1:Key2", "5.45" },
|
||||
{ "Decimal2:Key1", "6.733" },
|
||||
|
||||
{ "Double1:Key1", "4.12" },
|
||||
{ "Double1:Key2", "5.45" },
|
||||
{ "Double2:Key1", "6.733" },
|
||||
|
||||
{ "Boolean1:Key1", "true" },
|
||||
{ "Boolean1:Key2", "false" },
|
||||
{ "Boolean2:Key1", "true" },
|
||||
|
||||
{ "DateTime1:Key1", "1987-11-20 08:00:00" },
|
||||
{ "DateTime1:Key2", "2000-08-12 12:00:00" },
|
||||
{ "DateTime2:Key1", "2018-01-01 20:30:10" },
|
||||
});
|
||||
_config = new ConfigHelper(environment, configuration, Substitute.For<ILogger<ConfigHelper>>());
|
||||
|
||||
_database.GetSetting("Setting1").Returns(new SettingModel() { Value = "001" });
|
||||
_database.GetSetting("Setting2").Returns(new SettingModel() { Value = "002" });
|
||||
_database.GetSetting("Version").Returns(new SettingModel() { Value = "v4.0.0" });
|
||||
|
||||
_database.AddSetting(Arg.Any<SettingModel>()).Returns(new SettingModel() { Value = "Added" });
|
||||
_database.EditSetting(Arg.Any<SettingModel>()).Returns(new SettingModel() { Value = "Updated" });
|
||||
_database.SetSetting(Arg.Any<SettingModel>()).Returns(new SettingModel() { Value = "Set" });
|
||||
_database.DeleteSetting(Arg.Any<SettingModel>()).Returns(true);
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
}
|
||||
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", null)]
|
||||
[TestCase("Setting1", "001")]
|
||||
[TestCase("Setting2", "002")]
|
||||
public async Task SettingsHelper_GetSetting(string key, string expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).Get(key);
|
||||
|
||||
Assert.That(actual?.Value, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase(null, null)]
|
||||
[TestCase("", null)]
|
||||
[TestCase("Setting1", "Set")]
|
||||
public async Task SettingsHelper_SetSetting(string key, string expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).SetSetting(key, "FakeValue");
|
||||
|
||||
Assert.That(actual?.Value, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Setting1", true)]
|
||||
public async Task SettingsHelper_DeleteSetting(string key, bool expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).DeleteSetting(key);
|
||||
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("String1:Key1", "Default", "Value1")]
|
||||
[TestCase("String1:Key2", "Default", "Value2")]
|
||||
[TestCase("String2:Key1", "Default", "Value3")]
|
||||
[TestCase("String2:Key2", "Default", "Default")]
|
||||
[TestCase("Release:Version", "v0.0.0", "v1.0.0")]
|
||||
[TestCase("Release:Plugin:Version", "v0.0.0", "v3.0.0")]
|
||||
public async Task SettingsHelper_GetOrDefault(string key, string defaultValue, string expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Int1:Key1", 999, 1)]
|
||||
[TestCase("Int1:Key2", 999, 2)]
|
||||
[TestCase("Int2:Key1", 999, 3)]
|
||||
[TestCase("Int2:Key2", 999, 999)]
|
||||
public async Task SettingsHelper_GetOrDefault(string key, int defaultValue, int expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Long1:Key1", 999, 4)]
|
||||
[TestCase("Long1:Key2", 999, 5)]
|
||||
[TestCase("Long2:Key1", 999, 6)]
|
||||
[TestCase("Long2:Key2", 999, 999)]
|
||||
public async Task SettingsHelper_GetOrDefault(string key, long defaultValue, long expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Decimal1:Key1", 999, 4.12)]
|
||||
[TestCase("Decimal1:Key2", 999, 5.45)]
|
||||
[TestCase("Decimal2:Key1", 999, 6.733)]
|
||||
[TestCase("Decimal2:Key2", 999, 999)]
|
||||
public async Task SettingsHelper_GetOrDefault(string key, decimal defaultValue, decimal expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Double1:Key1", 999, 4.12)]
|
||||
[TestCase("Double1:Key2", 999, 5.45)]
|
||||
[TestCase("Double2:Key1", 999, 6.733)]
|
||||
[TestCase("Double2:Key2", 999, 999)]
|
||||
public async Task SettingsHelper_GetOrDefault(string key, double defaultValue, double expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("Boolean1:Key1", false, true)]
|
||||
[TestCase("Boolean1:Key2", false, false)]
|
||||
[TestCase("Boolean2:Key1", false, true)]
|
||||
[TestCase("Boolean2:Key2", true, true)]
|
||||
public async Task SettingsHelper_GetOrDefault(string key, bool defaultValue, bool expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("DateTime1:Key1", null, "1987-11-20 08:00:00")]
|
||||
[TestCase("DateTime1:Key2", null, "2000-08-12 12:00:00")]
|
||||
[TestCase("DateTime2:Key1", null, "2018-01-01 20:30:10")]
|
||||
[TestCase("DateTime2:Key2", null, null)]
|
||||
[TestCase("DateTime3:Key3", "2350-05-05 00:05:12", "2350-05-05 00:05:12")]
|
||||
public async Task SettingsHelper_GetOrDefault(string key, DateTime? defaultValue, string? expected)
|
||||
{
|
||||
var actual = await new SettingsHelper(_database, _config, _logger).GetOrDefault(key, defaultValue);
|
||||
Assert.That(actual, Is.EqualTo(!string.IsNullOrWhiteSpace(expected) ? DateTime.Parse(expected) : null));
|
||||
}
|
||||
}
|
||||
}
|
||||
136
WeddingShare.UnitTests/Tests/Helpers/UrlHelper.cs
Normal file
136
WeddingShare.UnitTests/Tests/Helpers/UrlHelper.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.UnitTests.Helpers;
|
||||
|
||||
namespace WeddingShare.UnitTests.Tests.Helpers
|
||||
{
|
||||
public class UrlHelperTests
|
||||
{
|
||||
private readonly ISettingsHelper _settings = Substitute.For<ISettingsHelper>();
|
||||
|
||||
public UrlHelperTests()
|
||||
{
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_settings.GetOrDefault(Settings.Basic.ForceHttps, Arg.Any<bool>()).Returns(false);
|
||||
}
|
||||
|
||||
[TestCase("http", "unittest.com", null, "http://unittest.com/")]
|
||||
[TestCase("https", "unittest.org", null, "https://unittest.org/")]
|
||||
[TestCase("http", "www.unittest.com", null, "http://www.unittest.com/")]
|
||||
[TestCase("https", "mobile.unittest.org", null, "https://mobile.unittest.org/")]
|
||||
[TestCase("http", "unittest.com", "", "http://unittest.com/")]
|
||||
[TestCase("https", "unittest.org", "", "https://unittest.org/")]
|
||||
[TestCase("http", "www.unittest.com", "", "http://www.unittest.com/")]
|
||||
[TestCase("https", "mobile.unittest.org", "", "https://mobile.unittest.org/")]
|
||||
[TestCase("http", "unittest.com", "/unittest", "http://unittest.com/unittest")]
|
||||
[TestCase("https", "unittest.org", "?unit=test", "https://unittest.org/?unit=test")]
|
||||
[TestCase("http", "www.unittest.com", "?unit=test&blaa=test", "http://www.unittest.com/?unit=test&blaa=test")]
|
||||
[TestCase("https", "mobile.unittest.org", "/unittest?unit=test&blaa=test", "https://mobile.unittest.org/unittest?unit=test&blaa=test")]
|
||||
public void UrlHelper_GenerateBaseUrl(string scheme, string host, string? querystring, string expected)
|
||||
{
|
||||
_settings.GetOrDefault(Settings.Basic.BaseUrl, Arg.Any<string>()).Returns(host);
|
||||
|
||||
var mockContext = MockData.MockHttpContext();
|
||||
mockContext.Request.Scheme = scheme;
|
||||
mockContext.Request.Host = new HostString(host);
|
||||
|
||||
var actual = new UrlHelper(_settings).GenerateBaseUrl(mockContext?.Request, querystring);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("?a=123&b=456&c=789", "d=111", "", "?a=123&b=456&c=789&d=111")]
|
||||
[TestCase("?a=123&b=456&c=789", "d=111,e=222", "", "?a=123&b=456&c=789&d=111&e=222")]
|
||||
[TestCase("?a=123&b=456&c=789", "e=222,f=333,d=111", "", "?a=123&b=456&c=789&e=222&f=333&d=111")]
|
||||
[TestCase("?a=123&b=456&c=789", "d=t$&", "", "?a=123&b=456&c=789&d=t%24%26")]
|
||||
public void UrlHelper_GenerateQueryString_Append(string queryString, string append, string exclude, string expected)
|
||||
{
|
||||
var mockContext = MockData.MockHttpContext();
|
||||
mockContext.Request.Scheme = "https";
|
||||
mockContext.Request.Host = new HostString("unit.test.com");
|
||||
mockContext.Request.QueryString = new QueryString(queryString);
|
||||
|
||||
var include = append?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.Select(x => {
|
||||
var val = x.Split('=');
|
||||
return new KeyValuePair<string, string>(val.FirstOrDefault() ?? "default", val.LastOrDefault() ?? "default");
|
||||
})?.ToList();
|
||||
|
||||
var actual = new UrlHelper(_settings).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("?a=123&b=456&c=789", "", "", "?a=123&b=456&c=789")]
|
||||
[TestCase("?a=123&b=456&c=789", "", "d", "?a=123&b=456&c=789")]
|
||||
[TestCase("?a=123&b=456&c=789", "", "a", "?b=456&c=789")]
|
||||
[TestCase("?a=123&b=456&c=789", "", "b", "?a=123&c=789")]
|
||||
[TestCase("?a=123&b=456&c=789", "", "c", "?a=123&b=456")]
|
||||
public void UrlHelper_GenerateQueryString_Exclude(string queryString, string append, string exclude, string expected)
|
||||
{
|
||||
var mockContext = MockData.MockHttpContext();
|
||||
mockContext.Request.Scheme = "https";
|
||||
mockContext.Request.Host = new HostString("unit.test.com");
|
||||
mockContext.Request.QueryString = new QueryString(queryString);
|
||||
|
||||
var include = append?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.Select(x => {
|
||||
var val = x.Split('=');
|
||||
return new KeyValuePair<string, string>(val.FirstOrDefault() ?? "default", val.LastOrDefault() ?? "default");
|
||||
})?.ToList();
|
||||
|
||||
var actual = new UrlHelper(_settings).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("?a=123&b=456&c=789", "d=111", "a", "?b=456&c=789&d=111")]
|
||||
[TestCase("?a=123&b=456&c=789", "d=111,e=222", "b", "?a=123&c=789&d=111&e=222")]
|
||||
[TestCase("?a=123&b=456&c=789", "e=222,f=333,d=111", "c", "?a=123&b=456&e=222&f=333&d=111")]
|
||||
[TestCase("?a=123&b=456&c=789", "d=111,e=222,f=333", "e", "?a=123&b=456&c=789&d=111&e=222&f=333")]
|
||||
public void UrlHelper_GenerateQueryString_AppendExclude(string queryString, string append, string exclude, string expected)
|
||||
{
|
||||
var mockContext = MockData.MockHttpContext();
|
||||
mockContext.Request.Scheme = "https";
|
||||
mockContext.Request.Host = new HostString("unit.test.com");
|
||||
mockContext.Request.QueryString = new QueryString(queryString);
|
||||
|
||||
var include = append?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.Select(x => {
|
||||
var val = x.Split('=');
|
||||
return new KeyValuePair<string, string>(val.FirstOrDefault() ?? "default", val.LastOrDefault() ?? "default");
|
||||
})?.ToList();
|
||||
|
||||
var actual = new UrlHelper(_settings).GenerateQueryString(mockContext?.Request, include, exclude?.Split(',', StringSplitOptions.RemoveEmptyEntries)?.ToList());
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("unit.test.com", "unit.test.com")]
|
||||
[TestCase("unit.test.com/", "unit.test.com")]
|
||||
[TestCase("http://unit.test.com", "unit.test.com")]
|
||||
[TestCase("http://unit.test.com/", "unit.test.com")]
|
||||
[TestCase("https://unit.test.com", "unit.test.com")]
|
||||
[TestCase("https://unit.test.com/", "unit.test.com")]
|
||||
[TestCase("http://test.com/", "test.com")]
|
||||
[TestCase("https://test.com/", "test.com")]
|
||||
public void UrlHelper_ExtractHost(string host, string expected)
|
||||
{
|
||||
var actual = new UrlHelper(_settings).ExtractHost(host);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[TestCase("?a=123&b=456&c=789", "a", "123")]
|
||||
[TestCase("?a=123&b=456&c=789", "b", "456")]
|
||||
[TestCase("?a=123&b=456&c=789", "c", "789")]
|
||||
[TestCase("?a=123&b=456&c=789", "d", "")]
|
||||
public void UrlHelper_ExtractHost(string queryString, string key, string expected)
|
||||
{
|
||||
var mockContext = MockData.MockHttpContext();
|
||||
mockContext.Request.Scheme = "https";
|
||||
mockContext.Request.Host = new HostString("unit.test.com");
|
||||
mockContext.Request.QueryString = new QueryString(queryString);
|
||||
|
||||
var actual = new UrlHelper(_settings).ExtractQueryValue(mockContext?.Request, key);
|
||||
Assert.That(actual, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,13 +10,14 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="dbup-sqlite" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.6.1" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
|
||||
namespace WeddingShare.Attributes
|
||||
{
|
||||
public class AllowGuestCreateAttribute : ActionFilterAttribute
|
||||
{
|
||||
public override void OnActionExecuting(ActionExecutingContext filterContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var request = filterContext.HttpContext.Request;
|
||||
|
||||
var galleryId = (request.Query.ContainsKey("id") && !string.IsNullOrWhiteSpace(request.Query["id"])) ? request.Query["id"].ToString().ToLower() : "default";
|
||||
|
||||
if (!string.Equals("default", galleryId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var user = filterContext?.HttpContext?.User;
|
||||
if (user?.Identity == null || !user.Identity.IsAuthenticated)
|
||||
{
|
||||
var configHelper = filterContext.HttpContext.RequestServices.GetService<IConfigHelper>();
|
||||
if (configHelper != null)
|
||||
{
|
||||
if (configHelper.GetOrDefault("Settings", "Disable_Guest_Gallery_Creation", true))
|
||||
{
|
||||
var databaseHelper = filterContext.HttpContext.RequestServices.GetService<IDatabaseHelper>();
|
||||
if (databaseHelper != null)
|
||||
{
|
||||
var gallery = databaseHelper.GetGallery(galleryId).Result;
|
||||
if (gallery == null)
|
||||
{
|
||||
filterContext.Result = new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.GalleryCreationNotAllowed }, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = filterContext.HttpContext.RequestServices.GetService<ILogger<RequiresSecretKeyAttribute>>();
|
||||
if (logger != null)
|
||||
{
|
||||
logger.LogError(ex, $"Failed to check guest creation - {ex?.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Web;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
|
||||
namespace WeddingShare.Attributes
|
||||
{
|
||||
@@ -12,22 +15,48 @@ namespace WeddingShare.Attributes
|
||||
{
|
||||
var request = filterContext.HttpContext.Request;
|
||||
|
||||
var secretKeyHelper = filterContext.HttpContext.RequestServices.GetService<ISecretKeyHelper>();
|
||||
if (secretKeyHelper != null)
|
||||
{
|
||||
var galleryId = (request.Query.ContainsKey("id") && !string.IsNullOrWhiteSpace(request.Query["id"])) ? request.Query["id"].ToString().ToLower() : "default";
|
||||
var secretKey = secretKeyHelper.GetGallerySecretKey(galleryId).Result;
|
||||
var galleryId = (request.Query.ContainsKey("id") && !string.IsNullOrWhiteSpace(request.Query["id"])) ? request.Query["id"].ToString().ToLower() : "default";
|
||||
|
||||
var key = request.Query.ContainsKey("key") ? request.Query["key"].ToString() : string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(secretKey) && !string.Equals(secretKey, key))
|
||||
{
|
||||
var logger = filterContext.HttpContext.RequestServices.GetService<ILogger<RequiresSecretKeyAttribute>>();
|
||||
if (logger != null)
|
||||
var databaseHelper = filterContext.HttpContext.RequestServices.GetService<IDatabaseHelper>();
|
||||
var gallery = databaseHelper?.GetGallery(galleryId).Result;
|
||||
if (gallery != null)
|
||||
{
|
||||
var encryptionHelper = filterContext.HttpContext.RequestServices.GetService<IEncryptionHelper>();
|
||||
if (encryptionHelper != null)
|
||||
{
|
||||
var key = request.Query.ContainsKey("key") ? request.Query["key"].ToString() : string.Empty;
|
||||
|
||||
var isEncrypted = request.Query.ContainsKey("enc") ? bool.Parse(request.Query["enc"].ToString().ToLower()) : false;
|
||||
if (!isEncrypted && !string.IsNullOrWhiteSpace(key) && encryptionHelper.IsEncryptionEnabled())
|
||||
{
|
||||
logger.LogWarning($"A request was made to an endpoint with an invalid secure key");
|
||||
}
|
||||
var queryString = HttpUtility.ParseQueryString(request.QueryString.ToString());
|
||||
queryString.Set("enc", "true");
|
||||
queryString.Set("key", encryptionHelper.Encrypt(key));
|
||||
|
||||
filterContext.Result = new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.InvalidSecretKey }, false);
|
||||
filterContext.Result = new RedirectResult($"/Gallery?{queryString.ToString()}");
|
||||
}
|
||||
else
|
||||
{
|
||||
var settingsHelper = filterContext.HttpContext.RequestServices.GetService<ISettingsHelper>();
|
||||
if (settingsHelper != null)
|
||||
{
|
||||
var secretKey = settingsHelper.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, galleryId).Result ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(secretKey))
|
||||
{
|
||||
secretKey = encryptionHelper.IsEncryptionEnabled() ? encryptionHelper.Encrypt(secretKey) : secretKey;
|
||||
if (!string.IsNullOrWhiteSpace(secretKey) && !string.Equals(secretKey, key))
|
||||
{
|
||||
var logger = filterContext.HttpContext.RequestServices.GetService<ILogger<RequiresSecretKeyAttribute>>();
|
||||
if (logger != null)
|
||||
{
|
||||
logger.LogWarning($"A request was made to an endpoint with an invalid secure key");
|
||||
}
|
||||
|
||||
filterContext.Result = new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.InvalidSecretKey }, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
WeddingShare/BackgroundWorkers/CleanupService.cs
Normal file
53
WeddingShare/BackgroundWorkers/CleanupService.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using NCrontab;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
|
||||
namespace WeddingShare.BackgroundWorkers
|
||||
{
|
||||
public sealed class CleanupService(IWebHostEnvironment hostingEnvironment, ISettingsHelper settingsHelper, IFileHelper fileHelper, ILogger<CleanupService> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var cron = await settingsHelper.GetOrDefault(BackgroundServices.Schedules.Cleanup, "0 4 * * *");
|
||||
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
|
||||
|
||||
await Task.Delay((int)TimeSpan.FromSeconds(10).TotalMilliseconds, stoppingToken);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var nextExecutionTime = schedule.GetNextOccurrence(now);
|
||||
var waitTime = nextExecutionTime - now;
|
||||
await Task.Delay(waitTime, stoppingToken);
|
||||
|
||||
await Cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task Cleanup()
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var paths = new List<string>()
|
||||
{
|
||||
Path.Combine(hostingEnvironment.WebRootPath, "temp")
|
||||
};
|
||||
|
||||
if (paths != null)
|
||||
{
|
||||
foreach (var path in paths)
|
||||
{
|
||||
try
|
||||
{
|
||||
fileHelper.DeleteDirectoryIfExists(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"An error occurred while running cleanup of '{path}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using NCrontab;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Enums;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
@@ -8,11 +9,11 @@ using WeddingShare.Models.Database;
|
||||
|
||||
namespace WeddingShare.BackgroundWorkers
|
||||
{
|
||||
public sealed class DirectoryScanner(IWebHostEnvironment hostingEnvironment, IConfigHelper configHelper, IDatabaseHelper databaseHelper, IImageHelper imageHelper, ILogger<DirectoryScanner> logger) : BackgroundService
|
||||
public sealed class DirectoryScanner(IWebHostEnvironment hostingEnvironment, ISettingsHelper settingsHelper, IDatabaseHelper databaseHelper, IFileHelper fileHelper, IImageHelper imageHelper, ILogger<DirectoryScanner> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
var cron = configHelper.GetOrDefault("BackgroundServices", "Directory_Scanner_Interval", "*/30 * * * *");
|
||||
var cron = settingsHelper.GetOrDefault(BackgroundServices.Schedules.DirectoryScanner, "*/30 * * * *").Result;
|
||||
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
|
||||
|
||||
await Task.Delay((int)TimeSpan.FromSeconds(10).TotalMilliseconds, stoppingToken);
|
||||
@@ -33,105 +34,118 @@ namespace WeddingShare.BackgroundWorkers
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var allowedFileTypes = configHelper.GetOrDefault("Settings", "Allowed_File_Types", ".jpg,.jpeg,.png").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var thumbnailsDirectory = Path.Combine(hostingEnvironment.WebRootPath, "thumbnails");
|
||||
if (!Directory.Exists(thumbnailsDirectory))
|
||||
if (Startup.Ready)
|
||||
{
|
||||
Directory.CreateDirectory(thumbnailsDirectory);
|
||||
}
|
||||
var thumbnailsDirectory = Path.Combine(hostingEnvironment.WebRootPath, "thumbnails");
|
||||
fileHelper.CreateDirectoryIfNotExists(thumbnailsDirectory);
|
||||
|
||||
var uploadsDirectory = Path.Combine(hostingEnvironment.WebRootPath, "uploads");
|
||||
if (Directory.Exists(uploadsDirectory))
|
||||
{
|
||||
var searchPattern = !configHelper.GetOrDefault("Settings", "Single_Gallery_Mode", false) ? "*" : "default";
|
||||
var galleries = Directory.GetDirectories(uploadsDirectory, searchPattern, SearchOption.TopDirectoryOnly)?.Where(x => !Path.GetFileName(x).StartsWith("."));
|
||||
if (galleries != null)
|
||||
var uploadsDirectory = Path.Combine(hostingEnvironment.WebRootPath, "uploads");
|
||||
if (fileHelper.DirectoryExists(uploadsDirectory))
|
||||
{
|
||||
foreach (var gallery in galleries)
|
||||
var searchPattern = !settingsHelper.GetOrDefault(Settings.Basic.SingleGalleryMode, false).Result ? "*" : "default";
|
||||
var galleries = fileHelper.GetDirectories(uploadsDirectory, searchPattern, SearchOption.TopDirectoryOnly)?.Where(x => !Path.GetFileName(x).StartsWith("."));
|
||||
if (galleries != null)
|
||||
{
|
||||
try
|
||||
foreach (var gallery in galleries)
|
||||
{
|
||||
var id = Path.GetFileName(gallery).ToLower();
|
||||
var galleryItem = await databaseHelper.GetGallery(id);
|
||||
if (galleryItem == null)
|
||||
try
|
||||
{
|
||||
galleryItem = await databaseHelper.AddGallery(new GalleryModel()
|
||||
var id = Path.GetFileName(gallery).ToLower();
|
||||
var galleryItem = await databaseHelper.GetGallery(id);
|
||||
if (galleryItem == null)
|
||||
{
|
||||
Name = id
|
||||
});
|
||||
}
|
||||
|
||||
if (galleryItem != null)
|
||||
{
|
||||
var galleryItems = await databaseHelper.GetAllGalleryItems(galleryItem.Id);
|
||||
|
||||
if (Path.Exists(gallery))
|
||||
{
|
||||
var approvedFiles = Directory.GetFiles(gallery, "*.*", SearchOption.TopDirectoryOnly).Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
|
||||
if (approvedFiles != null)
|
||||
if (await databaseHelper.GetGalleryCount() < await settingsHelper.GetOrDefault(Settings.Basic.MaxGalleryCount, 1000000))
|
||||
{
|
||||
foreach (var file in approvedFiles)
|
||||
galleryItem = await databaseHelper.AddGallery(new GalleryModel()
|
||||
{
|
||||
try
|
||||
{
|
||||
var filename = Path.GetFileName(file);
|
||||
if (!galleryItems.Exists(x => string.Equals(x.Title, filename, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
await databaseHelper.AddGalleryItem(new GalleryItemModel()
|
||||
{
|
||||
GalleryId = galleryItem.Id,
|
||||
Title = filename,
|
||||
State = GalleryItemState.Approved
|
||||
});
|
||||
}
|
||||
|
||||
var thumbnailPath = Path.Combine(thumbnailsDirectory, $"{Path.GetFileNameWithoutExtension(file)}.webp");
|
||||
if (!File.Exists(thumbnailPath))
|
||||
{
|
||||
await imageHelper.GenerateThumbnail(file, thumbnailPath, configHelper.GetOrDefault("Settings", "Thumbnail_Size", 720));
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var img = await Image.LoadAsync(thumbnailPath))
|
||||
{
|
||||
var width = img.Width;
|
||||
|
||||
img.Mutate(x => x.AutoOrient());
|
||||
|
||||
if (width != img.Width)
|
||||
{
|
||||
await img.SaveAsWebpAsync(thumbnailPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"An error occurred while scanning file '{file}'");
|
||||
}
|
||||
}
|
||||
Name = id
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (Path.Exists(Path.Combine(gallery, "Pending")))
|
||||
if (galleryItem != null)
|
||||
{
|
||||
var allowedFileTypes = settingsHelper.GetOrDefault(Settings.Gallery.AllowedFileTypes, ".jpg,.jpeg,.png,.mp4,.mov", galleryItem?.Name).Result.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
var galleryItems = await databaseHelper.GetAllGalleryItems(galleryItem.Id);
|
||||
|
||||
if (Path.Exists(gallery))
|
||||
{
|
||||
var pendingFiles = Directory.GetFiles(Path.Combine(gallery, "Pending"), "*.*", SearchOption.TopDirectoryOnly).Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
|
||||
if (pendingFiles != null)
|
||||
var approvedFiles = fileHelper.GetFiles(gallery, "*.*", SearchOption.TopDirectoryOnly).Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
|
||||
if (approvedFiles != null)
|
||||
{
|
||||
foreach (var file in pendingFiles)
|
||||
foreach (var file in approvedFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filename = Path.GetFileName(file);
|
||||
if (!galleryItems.Exists(x => string.Equals(x.Title, filename, StringComparison.OrdinalIgnoreCase)))
|
||||
var g = galleryItems.FirstOrDefault(x => string.Equals(x.Title, filename, StringComparison.OrdinalIgnoreCase));
|
||||
if (g == null)
|
||||
{
|
||||
await databaseHelper.AddGalleryItem(new GalleryItemModel()
|
||||
g = await databaseHelper.AddGalleryItem(new GalleryItemModel()
|
||||
{
|
||||
GalleryId = galleryItem.Id,
|
||||
Title = filename,
|
||||
State = GalleryItemState.Pending
|
||||
Checksum = await fileHelper.GetChecksum(file),
|
||||
MediaType = imageHelper.GetMediaType(file),
|
||||
State = GalleryItemState.Approved,
|
||||
UploadedDate = await fileHelper.GetCreationDatetime(file),
|
||||
FileSize = fileHelper.FileSize(file),
|
||||
});
|
||||
}
|
||||
|
||||
var thumbnailPath = Path.Combine(thumbnailsDirectory, $"{Path.GetFileNameWithoutExtension(file)}.webp");
|
||||
if (!fileHelper.FileExists(thumbnailPath))
|
||||
{
|
||||
await imageHelper.GenerateThumbnail(file, thumbnailPath, settingsHelper.GetOrDefault(Settings.Basic.ThumbnailSize, 720).Result);
|
||||
}
|
||||
else
|
||||
{
|
||||
using (var img = await Image.LoadAsync(thumbnailPath))
|
||||
{
|
||||
var width = img.Width;
|
||||
|
||||
img.Mutate(x => x.AutoOrient());
|
||||
|
||||
if (width != img.Width)
|
||||
{
|
||||
await img.SaveAsWebpAsync(thumbnailPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (g != null)
|
||||
{
|
||||
var updated = false;
|
||||
|
||||
if (g.UploadedDate == null)
|
||||
{
|
||||
g.UploadedDate = new FileInfo(file).CreationTimeUtc;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (g.MediaType == MediaType.Unknown)
|
||||
{
|
||||
g.MediaType = imageHelper.GetMediaType(file);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (g.Orientation == ImageOrientation.None)
|
||||
{
|
||||
g.Orientation = await imageHelper.GetOrientation(thumbnailPath);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (g.FileSize == 0)
|
||||
{
|
||||
g.FileSize = fileHelper.FileSize(file);
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (updated)
|
||||
{
|
||||
await databaseHelper.EditGalleryItem(g);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -139,17 +153,53 @@ namespace WeddingShare.BackgroundWorkers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Path.Exists(Path.Combine(gallery, "Pending")))
|
||||
{
|
||||
var pendingFiles = fileHelper.GetFiles(Path.Combine(gallery, "Pending"), "*.*", SearchOption.TopDirectoryOnly).Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
|
||||
if (pendingFiles != null)
|
||||
{
|
||||
foreach (var file in pendingFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filename = Path.GetFileName(file);
|
||||
if (!galleryItems.Exists(x => string.Equals(x.Title, filename, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
await databaseHelper.AddGalleryItem(new GalleryItemModel()
|
||||
{
|
||||
GalleryId = galleryItem.Id,
|
||||
Title = filename,
|
||||
Checksum = await fileHelper.GetChecksum(file),
|
||||
MediaType = imageHelper.GetMediaType(file),
|
||||
State = GalleryItemState.Pending,
|
||||
UploadedDate = await fileHelper.GetCreationDatetime(file),
|
||||
FileSize = new FileInfo(file).Length
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"An error occurred while scanning file '{file}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"An error occurred while scanning directory '{gallery}'");
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, $"An error occurred while scanning directory '{gallery}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogInformation($"Skipping directory scan, application not ready yet");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
62
WeddingShare/BackgroundWorkers/NotificationReport.cs
Normal file
62
WeddingShare/BackgroundWorkers/NotificationReport.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System.Text;
|
||||
using NCrontab;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Helpers.Notifications;
|
||||
|
||||
namespace WeddingShare.BackgroundWorkers
|
||||
{
|
||||
public sealed class NotificationReport(ISettingsHelper settingsHelper, IDatabaseHelper databaseHelper, ISmtpClientWrapper smtpHelper, ILoggerFactory loggerFactory) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
if (await settingsHelper.GetOrDefault(Settings.Basic.EmailReport, true) && await settingsHelper.GetOrDefault(Notifications.Smtp.Enabled, false))
|
||||
{
|
||||
var cron = await settingsHelper.GetOrDefault(BackgroundServices.Schedules.EmailReport, "0 0 * * *");
|
||||
var schedule = CrontabSchedule.Parse(cron, new CrontabSchedule.ParseOptions() { IncludingSeconds = cron.Split(new[] { ' ' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).Length == 6 });
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
var now = DateTime.Now;
|
||||
var nextExecutionTime = schedule.GetNextOccurrence(now);
|
||||
var waitTime = nextExecutionTime - now;
|
||||
await Task.Delay(waitTime, stoppingToken);
|
||||
|
||||
await SendReport();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendReport()
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
var pendingItems = await databaseHelper.GetPendingGalleryItems();
|
||||
if (pendingItems != null && pendingItems.Any())
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine($"<h1>You have items pending review!</h1>");
|
||||
|
||||
foreach (var item in pendingItems.GroupBy(x => x.GalleryName).OrderBy(x => x.Key))
|
||||
{
|
||||
try
|
||||
{
|
||||
builder.AppendLine($"<p style=\"font-size: 16pt;\">{item.Key} - Pending Items ({item.Count()})</p>");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
loggerFactory.CreateLogger<NotificationReport>().LogError(ex, $"Failed to build gallery report for id '{item?.Key}' - {ex?.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
var sent = await new EmailHelper(settingsHelper, smtpHelper, loggerFactory.CreateLogger<EmailHelper>()).Send("Pending Items Report", builder.ToString());
|
||||
if (!sent)
|
||||
{
|
||||
loggerFactory.CreateLogger<NotificationReport>().LogWarning($"Failed to send notification report");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
32
WeddingShare/Configurations/DatabaseConfiguration.cs
Normal file
32
WeddingShare/Configurations/DatabaseConfiguration.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Helpers.Dbup;
|
||||
|
||||
namespace WeddingShare.Configurations
|
||||
{
|
||||
internal static class DatabaseConfiguration
|
||||
{
|
||||
public static IDatabaseHelper AddDatabaseConfiguration(this IServiceCollection services, IConfigHelper config, ILoggerFactory loggerFactory)
|
||||
{
|
||||
IDatabaseHelper helper;
|
||||
|
||||
var databaseType = config.GetOrDefault(Settings.Database.Type, "sqlite");
|
||||
switch (databaseType?.ToLower())
|
||||
{
|
||||
case "mysql":
|
||||
services.AddSingleton<IDatabaseHelper, MySqlDatabaseHelper>();
|
||||
helper = new MySqlDatabaseHelper(config, loggerFactory.CreateLogger<MySqlDatabaseHelper>());
|
||||
break;
|
||||
default:
|
||||
services.AddSingleton<IDatabaseHelper, SQLiteDatabaseHelper>();
|
||||
helper = new SQLiteDatabaseHelper(config, loggerFactory.CreateLogger<SQLiteDatabaseHelper>());
|
||||
break;
|
||||
}
|
||||
|
||||
services.AddHostedService<DbupMigrator>();
|
||||
|
||||
return helper;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using WeddingShare.Helpers;
|
||||
|
||||
namespace WeddingShare.Configurations
|
||||
{
|
||||
internal static class DependencyInjectionConfiguration
|
||||
{
|
||||
public static void AddDependencyInjectionConfiguration(this IServiceCollection services)
|
||||
{
|
||||
services.AddSingleton<IConfigHelper, ConfigHelper>();
|
||||
services.AddSingleton<IEnvironmentWrapper, EnvironmentWrapper>();
|
||||
services.AddSingleton<ISettingsHelper, SettingsHelper>();
|
||||
services.AddSingleton<IImageHelper, ImageHelper>();
|
||||
services.AddSingleton<IFileHelper, FileHelper>();
|
||||
services.AddSingleton<IDeviceDetector, DeviceDetector>();
|
||||
services.AddSingleton<ISmtpClientWrapper, SmtpClientWrapper>();
|
||||
services.AddSingleton<IEncryptionHelper, EncryptionHelper>();
|
||||
services.AddSingleton<IUrlHelper, UrlHelper>();
|
||||
services.AddSingleton<ILanguageHelper, LanguageHelper>();
|
||||
}
|
||||
}
|
||||
}
|
||||
55
WeddingShare/Configurations/LocalizationConfiguration.cs
Normal file
55
WeddingShare/Configurations/LocalizationConfiguration.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
|
||||
namespace WeddingShare.Configurations
|
||||
{
|
||||
public static class LocalizationConfiguration
|
||||
{
|
||||
public static string CurrentCulture = "en-GB";
|
||||
|
||||
public static void AddLocalizationConfiguration(this IServiceCollection services, SettingsHelper settings)
|
||||
{
|
||||
services.AddLocalization(options =>
|
||||
{
|
||||
options.ResourcesPath = "Resources";
|
||||
});
|
||||
|
||||
services.Configure<RequestLocalizationOptions>(options => {
|
||||
var supportedCultures = new LanguageHelper().DetectSupportedCultures();
|
||||
|
||||
var language = settings.GetOrDefault(Settings.Languages.Default, "en-GB").Result;
|
||||
CurrentCulture = GetDefaultCulture(supportedCultures, language);
|
||||
|
||||
options.DefaultRequestCulture = new RequestCulture(CurrentCulture);
|
||||
options.SupportedCultures = supportedCultures;
|
||||
options.SupportedUICultures = supportedCultures;
|
||||
});
|
||||
}
|
||||
|
||||
private static string GetDefaultCulture(List<CultureInfo> supported, string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var culture in supported)
|
||||
{
|
||||
if (CultureMatches(culture, key))
|
||||
{
|
||||
return culture.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return "en-GB";
|
||||
}
|
||||
|
||||
private static bool CultureMatches(CultureInfo culture, string key)
|
||||
{
|
||||
return string.Equals(culture.Name, key, StringComparison.OrdinalIgnoreCase) || string.Equals(culture.ThreeLetterISOLanguageName, key, StringComparison.OrdinalIgnoreCase) || string.Equals(culture.TwoLetterISOLanguageName, key, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
50
WeddingShare/Configurations/NotificationConfiguration.cs
Normal file
50
WeddingShare/Configurations/NotificationConfiguration.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Net.Http.Headers;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Notifications;
|
||||
|
||||
namespace WeddingShare.Configurations
|
||||
{
|
||||
internal static class NotificationConfiguration
|
||||
{
|
||||
private const int CLIENT_DEFAULT_TIMEOUT = 10;
|
||||
|
||||
public static void AddNotificationConfiguration(this IServiceCollection services, SettingsHelper settings)
|
||||
{
|
||||
services.AddSingleton<INotificationHelper, NotificationBroker>();
|
||||
services.AddNtfyConfiguration(settings);
|
||||
services.AddGotifyConfiguration(settings);
|
||||
}
|
||||
|
||||
public static void AddNtfyConfiguration(this IServiceCollection services, SettingsHelper settings)
|
||||
{
|
||||
services.AddHttpClient("NtfyClient", (serviceProvider, httpClient) =>
|
||||
{
|
||||
var endpoint = settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, string.Empty).Result;
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
var token = settings.GetOrDefault(Constants.Notifications.Ntfy.Token, string.Empty).Result;
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
}
|
||||
|
||||
httpClient.BaseAddress = new Uri(endpoint);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(CLIENT_DEFAULT_TIMEOUT);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void AddGotifyConfiguration(this IServiceCollection services, SettingsHelper settings)
|
||||
{
|
||||
services.AddHttpClient("GotifyClient", (serviceProvider, httpClient) =>
|
||||
{
|
||||
var endpoint = settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, string.Empty).Result;
|
||||
if (!string.IsNullOrWhiteSpace(endpoint))
|
||||
{
|
||||
httpClient.BaseAddress = new Uri(endpoint);
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(CLIENT_DEFAULT_TIMEOUT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
WeddingShare/Constants/BackgroundServices.cs
Normal file
13
WeddingShare/Constants/BackgroundServices.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace WeddingShare.Constants
|
||||
{
|
||||
public class BackgroundServices
|
||||
{
|
||||
public class Schedules
|
||||
{
|
||||
public const string DirectoryScanner = "BackgroundServices:Schedules:Directory_Scanner";
|
||||
public const string EmailReport = "BackgroundServices:Schedules:Email_Report";
|
||||
public const string Cleanup = "BackgroundServices:Schedules:Cleanup";
|
||||
public const string DemoSystemReset = "BackgroundServices:Schedules:Demo_System_Reset";
|
||||
}
|
||||
}
|
||||
}
|
||||
7
WeddingShare/Constants/FFMPEG.cs
Normal file
7
WeddingShare/Constants/FFMPEG.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace WeddingShare.Constants
|
||||
{
|
||||
public class FFMPEG
|
||||
{
|
||||
public const string InstallPath = "FFMPEG:InstallPath";
|
||||
}
|
||||
}
|
||||
43
WeddingShare/Constants/Notifications.cs
Normal file
43
WeddingShare/Constants/Notifications.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace WeddingShare.Constants
|
||||
{
|
||||
public class Notifications
|
||||
{
|
||||
public class Alerts
|
||||
{
|
||||
public const string FailedLogin = "Notifications:Alerts:Failed_Login";
|
||||
public const string AccountLockout = "Notifications:Alerts:Account_Lockout";
|
||||
public const string DestructiveAction = "Notifications:Alerts:Destructive_Action";
|
||||
public const string PendingReview = "Notifications:Alerts:Pending_Review";
|
||||
}
|
||||
|
||||
public class Gotify
|
||||
{
|
||||
public const string Enabled = "Notifications:Gotify:Enabled";
|
||||
public const string Endpoint = "Notifications:Gotify:Endpoint";
|
||||
public const string Token = "Notifications:Gotify:Token";
|
||||
public const string Priority = "Notifications:Gotify:Priority";
|
||||
}
|
||||
|
||||
public class Ntfy
|
||||
{
|
||||
public const string Enabled = "Notifications:Ntfy:Enabled";
|
||||
public const string Endpoint = "Notifications:Ntfy:Endpoint";
|
||||
public const string Token = "Notifications:Ntfy:Token";
|
||||
public const string Topic = "Notifications:Ntfy:Topic";
|
||||
public const string Priority = "Notifications:Ntfy:Priority";
|
||||
}
|
||||
|
||||
public class Smtp
|
||||
{
|
||||
public const string Enabled = "Notifications:Smtp:Enabled";
|
||||
public const string Recipient = "Notifications:Smtp:Recipient";
|
||||
public const string Host = "Notifications:Smtp:Host";
|
||||
public const string Port = "Notifications:Smtp:Port";
|
||||
public const string Username = "Notifications:Smtp:Username";
|
||||
public const string Password = "Notifications:Smtp:Password";
|
||||
public const string From = "Notifications:Smtp:From";
|
||||
public const string DisplayName = "Notifications:Smtp:DisplayName";
|
||||
public const string UseSSL = "Notifications:Smtp:Use_SSL";
|
||||
}
|
||||
}
|
||||
}
|
||||
7
WeddingShare/Constants/Release.cs
Normal file
7
WeddingShare/Constants/Release.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace WeddingShare.Constants
|
||||
{
|
||||
public class Release
|
||||
{
|
||||
public const string Version = "Release:Version";
|
||||
}
|
||||
}
|
||||
26
WeddingShare/Constants/Security.cs
Normal file
26
WeddingShare/Constants/Security.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
namespace WeddingShare.Constants
|
||||
{
|
||||
public class Security
|
||||
{
|
||||
public class Encryption
|
||||
{
|
||||
public const string Key = "Security:Encryption:Key";
|
||||
public const string Salt = "Security:Encryption:Salt";
|
||||
public const string Iterations = "Security:Encryption:Iterations";
|
||||
public const string HashType = "Security:Encryption:HashType";
|
||||
}
|
||||
|
||||
public class Headers
|
||||
{
|
||||
public const string Enabled = "Security:Headers:Enabled";
|
||||
public const string XFrameOptions = "Security:Headers:X_Frame_Options";
|
||||
public const string XContentTypeOptions = "Security:Headers:X_Content_Type_Options";
|
||||
public const string CSP = "Security:Headers:CSP";
|
||||
}
|
||||
|
||||
public class MultiFactor
|
||||
{
|
||||
public const string ResetToDefault = "Security:2FA:Reset_To_Default";
|
||||
}
|
||||
}
|
||||
}
|
||||
106
WeddingShare/Constants/Settings.cs
Normal file
106
WeddingShare/Constants/Settings.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
namespace WeddingShare.Constants
|
||||
{
|
||||
public class Settings
|
||||
{
|
||||
public const string IsDemoMode = "Settings:Demo_Mode";
|
||||
|
||||
public class Account
|
||||
{
|
||||
public const string ShowProfileIcon = "Settings:Account:Show_Profile_Icon";
|
||||
public const string LockoutAttempts = "Settings:Account:Lockout_Attempts";
|
||||
public const string LockoutMins = "Settings:Account:Lockout_Mins";
|
||||
|
||||
public class Admin
|
||||
{
|
||||
public const string Username = "Settings:Account:Admin:Username";
|
||||
public const string Password = "Settings:Account:Admin:Password";
|
||||
public const string LogPassword = "Settings:Account:Admin:Log_Password";
|
||||
}
|
||||
}
|
||||
|
||||
public class Basic
|
||||
{
|
||||
public const string Title = "Settings:Title";
|
||||
public const string Logo = "Settings:Logo";
|
||||
public const string BaseUrl = "Settings:Base_Url";
|
||||
public const string ForceHttps = "Settings:Force_Https";
|
||||
public const string SingleGalleryMode = "Settings:Single_Gallery_Mode";
|
||||
public const string MaxGalleryCount = "Settings:Max_Gallery_Count";
|
||||
public const string HomeLink = "Settings:Home_Link";
|
||||
public const string GuestGalleryCreation = "Settings:Guest_Gallery_Creation";
|
||||
public const string HideKeyFromQRCode = "Settings:Hide_Key_From_QR_Code";
|
||||
public const string LinksOpenNewTab = "Settings:Links_Open_New_Tab";
|
||||
public const string ThumbnailSize = "Settings:Thumbnail_Size";
|
||||
public const string EmailReport = "Settings:Email_Report";
|
||||
}
|
||||
|
||||
public class Database
|
||||
{
|
||||
public const string Type = "Settings:Database:Type";
|
||||
public const string ConnectionString = "Settings:Database:Connection_String";
|
||||
public const string SyncFromConfig = "Settings:Database:Sync_From_Config";
|
||||
}
|
||||
|
||||
public class Gallery
|
||||
{
|
||||
public const string BannerImage = "Settings:Gallery:Banner_Image";
|
||||
public const string Quote = "Settings:Gallery:Quote";
|
||||
public const string SecretKey = "Settings:Gallery:Secret_Key";
|
||||
public const string Columns = "Settings:Gallery:Columns";
|
||||
public const string ItemsPerPage = "Settings:Gallery:Items_Per_Page";
|
||||
public const string FullWidth = "Settings:Gallery:Full_Width";
|
||||
public const string RetainRejectedItems = "Settings:Gallery:Retain_Rejected_Items";
|
||||
public const string Upload = "Settings:Gallery:Upload";
|
||||
public const string Download = "Settings:Gallery:Download";
|
||||
public const string RequireReview = "Settings:Gallery:Require_Review";
|
||||
public const string ReviewCounter = "Settings:Gallery:Review_Counter";
|
||||
public const string PreventDuplicates = "Settings:Gallery:Prevent_Duplicates";
|
||||
public const string IdleRefreshMins = "Settings:Gallery:Idle_Refresh_Mins";
|
||||
public const string MaxSizeMB = "Settings:Gallery:Max_Size_MB";
|
||||
public const string MaxFileSizeMB = "Settings:Gallery:Max_File_Size_MB";
|
||||
public const string DefaultView = "Settings:Gallery:Default_View";
|
||||
public const string UploadPeriod = "Settings:Gallery:Upload_Period";
|
||||
public const string AllowedFileTypes = "Settings:Gallery:Allowed_File_Types";
|
||||
|
||||
public class QRCode
|
||||
{
|
||||
public const string Enabled = "Settings:Gallery:QR_Code:Enabled";
|
||||
public const string DefaultView = "Settings:Gallery:QR_Code:Default_View";
|
||||
public const string DefaultSort = "Settings:Gallery:QR_Code:Default_Sort";
|
||||
}
|
||||
}
|
||||
|
||||
public class GallerySelector
|
||||
{
|
||||
public const string Dropdown = "Settings:Gallery_Selector:Dropdown";
|
||||
public const string HideDefaultOption = "Settings:Gallery_Selector:Hide_Default_Option";
|
||||
}
|
||||
|
||||
public class IdentityCheck
|
||||
{
|
||||
public const string Enabled = "Settings:Identity_Check:Enabled";
|
||||
public const string ShowOnPageLoad = "Settings:Identity_Check:Show_On_Page_Load";
|
||||
public const string RequireIdentityForUpload = "Settings:Identity_Check:Require_Identity_For_Upload";
|
||||
}
|
||||
|
||||
public class Languages
|
||||
{
|
||||
public const string Enabled = "Settings:Languages:Enabled";
|
||||
public const string Default = "Settings:Languages:Default";
|
||||
}
|
||||
|
||||
public class Slideshow
|
||||
{
|
||||
public const string Interval = "Settings:Slideshow:Interval";
|
||||
public const string Fade = "Settings:Slideshow:Fade";
|
||||
public const string Limit = "Settings:Slideshow:Limit";
|
||||
public const string IncludeShareSlide = "Settings:Slideshow:Include_Share_Slide";
|
||||
}
|
||||
|
||||
public class Themes
|
||||
{
|
||||
public const string Enabled = "Settings:Themes:Enabled";
|
||||
public const string Default = "Settings:Themes:Default";
|
||||
}
|
||||
}
|
||||
}
|
||||
49
WeddingShare/Constants/ViewOptions.cs
Normal file
49
WeddingShare/Constants/ViewOptions.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
namespace WeddingShare.Constants
|
||||
{
|
||||
public class ViewOptions
|
||||
{
|
||||
public static IDictionary<string, string> YesNo = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Yes", "true" },
|
||||
{ "No", "false" }
|
||||
};
|
||||
|
||||
public static IDictionary<string, string> YesNoInverted = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Yes", "false" },
|
||||
{ "No", "true" }
|
||||
};
|
||||
|
||||
public static IDictionary<string, string> SingleGalleryMode = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Single", "true" },
|
||||
{ "Multiple", "false" }
|
||||
};
|
||||
|
||||
public static IDictionary<string, string> GallerySelectorDropdown = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Dropdown", "true" },
|
||||
{ "Input", "false" }
|
||||
};
|
||||
|
||||
public static IDictionary<string, string> GalleryWidth = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Full Width", "true" },
|
||||
{ "Default", "false" }
|
||||
};
|
||||
|
||||
public static IDictionary<string, string> GalleryDefaultView = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Default", "default" },
|
||||
{ "Presentation", "presentation" },
|
||||
{ "Slideshow", "slideshow" }
|
||||
};
|
||||
|
||||
public static IDictionary<string, string> GalleryDefaultSort = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Ascending", "0" },
|
||||
{ "Descending", "1" },
|
||||
{ "Random", "2" }
|
||||
};
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,15 @@
|
||||
using System.IO.Compression;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using WeddingShare.Attributes;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Enums;
|
||||
using WeddingShare.Extensions;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Helpers.Notifications;
|
||||
using WeddingShare.Models;
|
||||
using WeddingShare.Models.Database;
|
||||
|
||||
@@ -15,143 +19,285 @@ namespace WeddingShare.Controllers
|
||||
public class GalleryController : Controller
|
||||
{
|
||||
private readonly IWebHostEnvironment _hostingEnvironment;
|
||||
private readonly IConfigHelper _config;
|
||||
private readonly ISettingsHelper _settings;
|
||||
private readonly IDatabaseHelper _database;
|
||||
private readonly ISecretKeyHelper _secretKey;
|
||||
private readonly IFileHelper _fileHelper;
|
||||
private readonly IDeviceDetector _deviceDetector;
|
||||
private readonly IImageHelper _imageHelper;
|
||||
private readonly INotificationHelper _notificationHelper;
|
||||
private readonly IEncryptionHelper _encryptionHelper;
|
||||
private readonly Helpers.IUrlHelper _urlHelper;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IStringLocalizer<GalleryController> _localizer;
|
||||
private readonly IStringLocalizer<Lang.Translations> _localizer;
|
||||
|
||||
private readonly string TempDirectory;
|
||||
private readonly string UploadsDirectory;
|
||||
private readonly string ThumbnailsDirectory;
|
||||
|
||||
public GalleryController(IWebHostEnvironment hostingEnvironment, IConfigHelper config, IDatabaseHelper database, ISecretKeyHelper secretKey, IDeviceDetector deviceDetector, IImageHelper imageHelper, ILogger<GalleryController> logger, IStringLocalizer<GalleryController> localizer)
|
||||
public GalleryController(IWebHostEnvironment hostingEnvironment, ISettingsHelper settings, IDatabaseHelper database, IFileHelper fileHelper, IDeviceDetector deviceDetector, IImageHelper imageHelper, INotificationHelper notificationHelper, IEncryptionHelper encryptionHelper, Helpers.IUrlHelper urlHelper, ILogger<GalleryController> logger, IStringLocalizer<Lang.Translations> localizer)
|
||||
{
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
_config = config;
|
||||
_settings = settings;
|
||||
_database = database;
|
||||
_secretKey = secretKey;
|
||||
_fileHelper = fileHelper;
|
||||
_deviceDetector = deviceDetector;
|
||||
_imageHelper = imageHelper;
|
||||
_notificationHelper = notificationHelper;
|
||||
_encryptionHelper = encryptionHelper;
|
||||
_urlHelper = urlHelper;
|
||||
_logger = logger;
|
||||
_localizer = localizer;
|
||||
|
||||
TempDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "temp");
|
||||
UploadsDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "uploads");
|
||||
ThumbnailsDirectory = Path.Combine(_hostingEnvironment.WebRootPath, "thumbnails");
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> Login(string id = "default", string? key = null)
|
||||
{
|
||||
var append = new List<KeyValuePair<string, string>>()
|
||||
{
|
||||
new KeyValuePair<string, string>("id", id)
|
||||
};
|
||||
|
||||
GalleryModel? gallery = await _database.GetGallery(id);
|
||||
if (gallery == null)
|
||||
{
|
||||
if (await _settings.GetOrDefault(Settings.Basic.GuestGalleryCreation, false))
|
||||
{
|
||||
if (await _database.GetGalleryCount() < await _settings.GetOrDefault(Settings.Basic.MaxGalleryCount, 1000000))
|
||||
{
|
||||
await _database.AddGallery(new GalleryModel()
|
||||
{
|
||||
Name = id.ToLower(),
|
||||
SecretKey = key
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.GalleryLimitReached }, false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.GalleryCreationNotAllowed }, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
var enc = _encryptionHelper.IsEncryptionEnabled();
|
||||
append.Add(new KeyValuePair<string, string>("key", enc ? _encryptionHelper.Encrypt(key) : key));
|
||||
append.Add(new KeyValuePair<string, string>("enc", enc.ToString().ToLower()));
|
||||
}
|
||||
|
||||
var redirectUrl = _urlHelper.GenerateFullUrl(HttpContext.Request, "/Gallery", append);
|
||||
|
||||
return new JsonResult(new { success = true, redirectUrl });
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[RequiresSecretKey]
|
||||
[AllowGuestCreate]
|
||||
public async Task<IActionResult> Index(string id = "default", string? key = null, ViewMode? mode = null, GalleryOrder order = GalleryOrder.None)
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public async Task<IActionResult> Index(string id = "default", string? key = null, ViewMode? mode = null, GalleryGroup group = GalleryGroup.None, GalleryOrder order = GalleryOrder.Descending, GalleryFilter filter = GalleryFilter.All, bool partial = false)
|
||||
{
|
||||
id = id.ToLower();
|
||||
if (string.IsNullOrWhiteSpace(id) || _config.GetOrDefault("Settings", "Single_Gallery_Mode", false))
|
||||
{
|
||||
id = "default";
|
||||
}
|
||||
id = (!string.IsNullOrWhiteSpace(id) && !await _settings.GetOrDefault(Settings.Basic.SingleGalleryMode, false)) ? id.ToLower() : "default";
|
||||
|
||||
try
|
||||
{
|
||||
ViewBag.ViewMode = mode ?? (ViewMode)_config.GetOrDefault("Settings", "Default_Gallery_View", (int)ViewMode.Default);
|
||||
ViewBag.ViewMode = mode ?? (ViewMode)await _settings.GetOrDefault(Settings.Gallery.DefaultView, (int)ViewMode.Default, id);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ViewBag.ViewMode = ViewMode.Default;
|
||||
}
|
||||
|
||||
var deviceType = HttpContext.Session.GetString("DeviceType");
|
||||
var deviceType = HttpContext.Session.GetString(SessionKey.DeviceType);
|
||||
if (string.IsNullOrWhiteSpace(deviceType))
|
||||
{
|
||||
deviceType = (await _deviceDetector.ParseDeviceType(Request.Headers["User-Agent"].ToString())).ToString();
|
||||
HttpContext.Session.SetString("DeviceType", deviceType ?? "Desktop");
|
||||
HttpContext.Session.SetString(SessionKey.DeviceType, deviceType ?? "Desktop");
|
||||
}
|
||||
|
||||
ViewBag.IsMobile = !string.Equals("Desktop", deviceType, StringComparison.OrdinalIgnoreCase);
|
||||
ViewBag.SecretKey = key;
|
||||
|
||||
var galleryPath = Path.Combine(UploadsDirectory, id);
|
||||
if (!Directory.Exists(galleryPath))
|
||||
{
|
||||
Directory.CreateDirectory(galleryPath);
|
||||
Directory.CreateDirectory(Path.Combine(galleryPath, "Pending"));
|
||||
}
|
||||
_fileHelper.CreateDirectoryIfNotExists(galleryPath);
|
||||
_fileHelper.CreateDirectoryIfNotExists(Path.Combine(galleryPath, "Pending"));
|
||||
|
||||
GalleryModel? gallery = await _database.GetGallery(id);
|
||||
if (gallery == null)
|
||||
{
|
||||
gallery = await _database.AddGallery(new GalleryModel()
|
||||
{
|
||||
Name = id.ToLower(),
|
||||
SecretKey = key
|
||||
});
|
||||
}
|
||||
|
||||
if (gallery != null)
|
||||
{
|
||||
var allowedFileTypes = _config.GetOrDefault("Settings", "Allowed_File_Types", ".jpg,.jpeg,.png").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
{
|
||||
ViewBag.GalleryId = gallery.Name;
|
||||
|
||||
var images = (await _database.GetAllGalleryItems(gallery.Id, GalleryItemState.Approved))?.Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x.Title).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
|
||||
switch (order)
|
||||
var secretKey = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, gallery.Name);
|
||||
ViewBag.SecretKey = secretKey;
|
||||
|
||||
var currentPage = 1;
|
||||
try
|
||||
{
|
||||
case GalleryOrder.UploadedAsc:
|
||||
images = images?.OrderBy(x => x.Id);
|
||||
currentPage = int.Parse((Request.Query.ContainsKey("page") && !string.IsNullOrWhiteSpace(Request.Query["page"])) ? Request.Query["page"].ToString().ToLower() : "1");
|
||||
}
|
||||
catch { }
|
||||
|
||||
var mediaType = MediaType.All;
|
||||
if (mode == ViewMode.Slideshow)
|
||||
{
|
||||
mediaType = MediaType.Image;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (filter)
|
||||
{
|
||||
case GalleryFilter.Images:
|
||||
mediaType = MediaType.Image;
|
||||
break;
|
||||
case GalleryFilter.Videos:
|
||||
mediaType = MediaType.Video;
|
||||
break;
|
||||
default:
|
||||
mediaType = MediaType.All;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var orientation = ImageOrientation.None;
|
||||
switch (filter)
|
||||
{
|
||||
case GalleryFilter.Landscape:
|
||||
orientation = ImageOrientation.Landscape;
|
||||
break;
|
||||
case GalleryOrder.UploadedDesc:
|
||||
images = images?.OrderByDescending(x => x.Id);
|
||||
case GalleryFilter.Portrait:
|
||||
orientation = ImageOrientation.Portrait;
|
||||
break;
|
||||
case GalleryOrder.NameAsc:
|
||||
images = images?.OrderByDescending(x => x.Title);
|
||||
case GalleryFilter.Square:
|
||||
orientation = ImageOrientation.Square;
|
||||
break;
|
||||
case GalleryOrder.NameDesc:
|
||||
images = images?.OrderBy(x => x.Title);
|
||||
break;
|
||||
default:
|
||||
default:
|
||||
orientation = ImageOrientation.None;
|
||||
break;
|
||||
}
|
||||
|
||||
var model = new PhotoGallery(_config.GetOrDefault("Settings", "Gallery_Columns", 4), (ViewMode)ViewBag.ViewMode)
|
||||
var itemsPerPage = await _settings.GetOrDefault(Settings.Gallery.ItemsPerPage, 50, gallery?.Name);
|
||||
var allowedFileTypes = (await _settings.GetOrDefault(Settings.Gallery.AllowedFileTypes, ".jpg,.jpeg,.png,.mp4,.mov", gallery?.Name)).Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
var items = (await _database.GetAllGalleryItems(gallery?.Id, GalleryItemState.Approved, mediaType, orientation, group, order, itemsPerPage, currentPage))?.Where(x => allowedFileTypes.Any(y => string.Equals(Path.GetExtension(x.Title).Trim('.'), y.Trim('.'), StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
var isAdmin = User?.Identity != null && User.Identity.IsAuthenticated;
|
||||
|
||||
FileUploader? fileUploader = null;
|
||||
if (!string.Equals("All", gallery?.Name, StringComparison.OrdinalIgnoreCase) && (await _settings.GetOrDefault(Settings.Gallery.Upload, true, gallery?.Name) || isAdmin))
|
||||
{
|
||||
GalleryId = id,
|
||||
GalleryPath = $"/{galleryPath.Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}",
|
||||
ThumbnailsPath = $"/{ThumbnailsDirectory.Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}",
|
||||
Images = images?.Select(x => new PhotoGalleryImage() { Id = x.Id, Name = Path.GetFileName(x.Title), Path = x.Title })?.ToList(),
|
||||
PendingCount = gallery?.PendingItems ?? 0,
|
||||
FileUploader = !_config.GetOrDefault("Settings", "Disable_Upload", false) || (User?.Identity != null && User.Identity.IsAuthenticated) ? new FileUploader(id, "/Gallery/UploadImage") : null
|
||||
var uploadActvated = isAdmin;
|
||||
try
|
||||
{
|
||||
if (!uploadActvated)
|
||||
{
|
||||
var periods = (await _settings.GetOrDefault(Settings.Gallery.UploadPeriod, "1970-01-01 00:00", gallery?.Name))?.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (periods != null)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var timeRanges = period?.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
if (timeRanges != null && timeRanges.Length > 0)
|
||||
{
|
||||
var startDate = DateTime.Parse(timeRanges[0]).ToUniversalTime();
|
||||
|
||||
if (timeRanges.Length == 2)
|
||||
{
|
||||
var endDate = DateTime.Parse(timeRanges[1]).ToUniversalTime();
|
||||
if (now >= startDate && now < endDate)
|
||||
{
|
||||
uploadActvated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (timeRanges.Length == 1 && now >= startDate)
|
||||
{
|
||||
uploadActvated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
uploadActvated = true;
|
||||
}
|
||||
|
||||
if (uploadActvated)
|
||||
{
|
||||
fileUploader = new FileUploader(gallery?.Name ?? "default", secretKey, "/Gallery/UploadImage", await _settings.GetOrDefault(Settings.IdentityCheck.RequireIdentityForUpload, false));
|
||||
}
|
||||
}
|
||||
|
||||
var itemCounts = await _database.GetGalleryItemCount(gallery?.Id, GalleryItemState.All, mediaType, orientation);
|
||||
var model = new PhotoGallery()
|
||||
{
|
||||
GalleryId = gallery?.Id,
|
||||
GalleryName = gallery?.Name,
|
||||
Images = items?.Select(x => new PhotoGalleryImage()
|
||||
{
|
||||
Id = x.Id,
|
||||
GalleryId = x.GalleryId,
|
||||
GalleryName = x.GalleryName,
|
||||
Name = Path.GetFileName(x.Title),
|
||||
UploadedBy = x.UploadedBy,
|
||||
UploadDate = x.UploadedDate,
|
||||
ImagePath = $"/{Path.Combine(UploadsDirectory, x.GalleryName).Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}/{x.Title}",
|
||||
ThumbnailPath = $"/{ThumbnailsDirectory.Remove(_hostingEnvironment.WebRootPath).Replace('\\', '/').TrimStart('/')}/{Path.GetFileNameWithoutExtension(x.Title)}.webp",
|
||||
MediaType = x.MediaType
|
||||
})?.ToList(),
|
||||
CurrentPage = currentPage,
|
||||
ApprovedCount = (int)itemCounts["Approved"],
|
||||
PendingCount = (int)itemCounts["Pending"],
|
||||
ItemsPerPage = itemsPerPage,
|
||||
FileUploader = fileUploader,
|
||||
ViewMode = (ViewMode)ViewBag.ViewMode,
|
||||
GroupBy = group,
|
||||
OrderBy = order,
|
||||
Pagination = order != GalleryOrder.Random,
|
||||
LoadScripts = !partial
|
||||
};
|
||||
|
||||
return View(model);
|
||||
return partial ? PartialView("~/Views/Gallery/GalleryWrapper.cshtml", model) : View(model);
|
||||
}
|
||||
|
||||
return View(new PhotoGallery());
|
||||
return new RedirectToActionResult("Index", "Error", new { Reason = ErrorCode.InvalidGalleryId }, false);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> UploadImage()
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
|
||||
try
|
||||
{
|
||||
string galleryId = (Request?.Form?.FirstOrDefault(x => string.Equals("Id", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString()?.ToLower() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(galleryId))
|
||||
{
|
||||
return Json(new { success = true, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Gallery_Id"].Value } });
|
||||
return Json(new { success = false, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Gallery_Id"].Value } });
|
||||
}
|
||||
|
||||
|
||||
var gallery = await _database.GetGallery(galleryId);
|
||||
if (gallery != null)
|
||||
{
|
||||
var secretKey = await _secretKey.GetGallerySecretKey(galleryId);
|
||||
string key = (Request?.Form?.FirstOrDefault(x => string.Equals("SecretKey", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString()?.ToLower() ?? string.Empty;
|
||||
var secretKey = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, galleryId);
|
||||
string key = (Request?.Form?.FirstOrDefault(x => string.Equals("SecretKey", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString() ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(secretKey) && !string.Equals(secretKey, key))
|
||||
{
|
||||
return Json(new { success = true, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Secret_Key_Warning"].Value } });
|
||||
return Json(new { success = false, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Secret_Key_Warning"].Value } });
|
||||
}
|
||||
|
||||
string uploadedBy = HttpContext.Session.GetString(SessionKey.ViewerIdentity) ?? "Anonymous";
|
||||
|
||||
var files = Request?.Form?.Files;
|
||||
if (files != null && files.Count > 0)
|
||||
{
|
||||
var requiresReview = _config.GetOrDefault("Settings", "Require_Review", true);
|
||||
var requiresReview = await _settings.GetOrDefault(Settings.Gallery.RequireReview, true, galleryId);
|
||||
|
||||
var uploaded = 0;
|
||||
var errors = new List<string>();
|
||||
@@ -160,54 +306,65 @@ namespace WeddingShare.Controllers
|
||||
try
|
||||
{
|
||||
var extension = Path.GetExtension(file.FileName);
|
||||
var maxFilesSize = _config.GetOrDefault("Settings", "Max_File_Size_Mb", 10) * 1000000;
|
||||
var maxGallerySize = await _settings.GetOrDefault(Settings.Gallery.MaxSizeMB, 1024L, galleryId) * 1000000;
|
||||
var maxFilesSize = await _settings.GetOrDefault(Settings.Gallery.MaxFileSizeMB, 10L, galleryId) * 1000000;
|
||||
var galleryPath = Path.Combine(UploadsDirectory, gallery.Name);
|
||||
|
||||
var allowedFileTypes = _config.GetOrDefault("Settings", "Allowed_File_Types", ".jpg,.jpeg,.png").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
var allowedFileTypes = (await _settings.GetOrDefault(Settings.Gallery.AllowedFileTypes, ".jpg,.jpeg,.png,.mp4,.mov", galleryId)).Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
if (!allowedFileTypes.Any(x => string.Equals(x.Trim('.'), extension.Trim('.'), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
errors.Add($"{_localizer["File_Upload_Failed"].Value} '{Path.GetFileName(file.FileName)}'. {_localizer["Invalid_File_Type"].Value}");
|
||||
errors.Add($"{_localizer["File_Upload_Failed"].Value}. {_localizer["Invalid_File_Type"].Value}");
|
||||
}
|
||||
else if (file.Length > maxFilesSize)
|
||||
{
|
||||
errors.Add($"{_localizer["File_Upload_Failed"].Value} '{Path.GetFileName(file.FileName)}'. {_localizer["Max_File_Size"].Value} {maxFilesSize} bytes");
|
||||
errors.Add($"{_localizer["File_Upload_Failed"].Value}. {_localizer["Max_File_Size"].Value} {maxFilesSize} bytes");
|
||||
}
|
||||
else if ((_fileHelper.GetDirectorySize(galleryPath) + file.Length) > maxGallerySize)
|
||||
{
|
||||
errors.Add($"{_localizer["File_Upload_Failed"].Value}. {_localizer["Gallery_Full"].Value} {maxGallerySize} bytes");
|
||||
}
|
||||
else
|
||||
{
|
||||
var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}";
|
||||
var galleryPath = requiresReview ? Path.Combine(UploadsDirectory, gallery.Name, "Pending") : Path.Combine(UploadsDirectory, gallery.Name);
|
||||
if (!Directory.Exists(galleryPath))
|
||||
{
|
||||
Directory.CreateDirectory(galleryPath);
|
||||
}
|
||||
galleryPath = requiresReview ? Path.Combine(galleryPath, "Pending") : galleryPath;
|
||||
|
||||
_fileHelper.CreateDirectoryIfNotExists(galleryPath);
|
||||
|
||||
var filePath = Path.Combine(galleryPath, fileName);
|
||||
if (!string.IsNullOrWhiteSpace(filePath))
|
||||
{
|
||||
using (var fs = new FileStream(filePath, FileMode.Create))
|
||||
{
|
||||
await file.CopyToAsync(fs);
|
||||
}
|
||||
await _fileHelper.SaveFile(file, filePath, FileMode.Create);
|
||||
|
||||
if (!requiresReview)
|
||||
{
|
||||
if (!Directory.Exists(ThumbnailsDirectory))
|
||||
var checksum = await _fileHelper.GetChecksum(filePath);
|
||||
if (await _settings.GetOrDefault(Settings.Gallery.PreventDuplicates, true, galleryId) && (string.IsNullOrWhiteSpace(checksum) || await _database.GetGalleryItemByChecksum(gallery.Id, checksum) != null))
|
||||
{
|
||||
errors.Add($"{_localizer["File_Upload_Failed"].Value}. {_localizer["Duplicate_Item_Detected"].Value}");
|
||||
_fileHelper.DeleteFileIfExists(filePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
var savePath = Path.Combine(ThumbnailsDirectory, $"{Path.GetFileNameWithoutExtension(filePath)}.webp");
|
||||
|
||||
_fileHelper.CreateDirectoryIfNotExists(ThumbnailsDirectory);
|
||||
await _imageHelper.GenerateThumbnail(filePath, savePath, await _settings.GetOrDefault(Settings.Basic.ThumbnailSize, 720));
|
||||
|
||||
var item = await _database.AddGalleryItem(new GalleryItemModel()
|
||||
{
|
||||
Directory.CreateDirectory(ThumbnailsDirectory);
|
||||
GalleryId = gallery.Id,
|
||||
Title = fileName,
|
||||
UploadedBy = uploadedBy,
|
||||
UploadedDate = await _fileHelper.GetCreationDatetime(filePath),
|
||||
Checksum = checksum,
|
||||
MediaType = _imageHelper.GetMediaType(filePath),
|
||||
Orientation = await _imageHelper.GetOrientation(savePath),
|
||||
State = requiresReview ? GalleryItemState.Pending : GalleryItemState.Approved,
|
||||
FileSize = file.Length,
|
||||
});
|
||||
|
||||
if (item?.Id > 0)
|
||||
{
|
||||
uploaded++;
|
||||
}
|
||||
|
||||
await _imageHelper.GenerateThumbnail(filePath, Path.Combine(ThumbnailsDirectory, $"{Path.GetFileNameWithoutExtension(filePath)}.webp"), _config.GetOrDefault("Settings", "Thumbnail_Size", 720));
|
||||
}
|
||||
|
||||
var item = await _database.AddGalleryItem(new GalleryItemModel()
|
||||
{
|
||||
GalleryId = gallery.Id,
|
||||
Title = fileName,
|
||||
State = requiresReview ? GalleryItemState.Pending : GalleryItemState.Approved
|
||||
});
|
||||
|
||||
if (item?.Id > 0)
|
||||
{
|
||||
uploaded++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +375,9 @@ namespace WeddingShare.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
return Json(new { success = true, uploaded, requiresReview, errors });
|
||||
Response.StatusCode = (int)HttpStatusCode.OK;
|
||||
|
||||
return Json(new { success = uploaded > 0, uploaded, uploadedBy, requiresReview, errors });
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -237,5 +396,105 @@ namespace WeddingShare.Controllers
|
||||
|
||||
return Json(new { success = false, uploaded = 0 });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> UploadCompleted()
|
||||
{
|
||||
Response.StatusCode = (int)HttpStatusCode.BadRequest;
|
||||
|
||||
try
|
||||
{
|
||||
string galleryId = (Request?.Form?.FirstOrDefault(x => string.Equals("Id", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString()?.ToLower() ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(galleryId))
|
||||
{
|
||||
return Json(new { success = false, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Gallery_Id"].Value } });
|
||||
}
|
||||
|
||||
var gallery = await _database.GetGallery(galleryId);
|
||||
if (gallery != null)
|
||||
{
|
||||
var secretKey = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, galleryId);
|
||||
string key = (Request?.Form?.FirstOrDefault(x => string.Equals("SecretKey", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString() ?? string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(secretKey) && !string.Equals(secretKey, key))
|
||||
{
|
||||
return Json(new { success = false, uploaded = 0, errors = new List<string>() { _localizer["Invalid_Secret_Key_Warning"].Value } });
|
||||
}
|
||||
|
||||
var uploadedBy = HttpContext.Session.GetString(SessionKey.ViewerIdentity) ?? "Anonymous";
|
||||
var requiresReview = await _settings.GetOrDefault(Settings.Gallery.RequireReview, true, galleryId);
|
||||
|
||||
int uploaded = int.Parse((Request?.Form?.FirstOrDefault(x => string.Equals("Count", x.Key, StringComparison.OrdinalIgnoreCase)).Value)?.ToString() ?? "0");
|
||||
if (uploaded > 0 && requiresReview && await _settings.GetOrDefault(Notifications.Alerts.PendingReview, true))
|
||||
{
|
||||
await _notificationHelper.Send(_localizer["New_Items_Pending_Review"].Value, $"{uploaded} new item(s) have been uploaded to gallery '{gallery.Name}' by '{(!string.IsNullOrWhiteSpace(uploadedBy) ? uploadedBy : "Anonymous")}' and are awaiting your review.", _urlHelper.GenerateBaseUrl(HttpContext?.Request, "/Admin"));
|
||||
}
|
||||
|
||||
Response.StatusCode = (int)HttpStatusCode.OK;
|
||||
|
||||
return Json(new { success = true, counters = new { total = gallery?.TotalItems ?? 0, approved = gallery?.ApprovedItems ?? 0, pending = gallery?.PendingItems ?? 0 }, uploaded, uploadedBy, requiresReview });
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(new { success = false, uploaded = 0, errors = new List<string>() { _localizer["Gallery_Does_Not_Exist"].Value } });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{_localizer["Image_Upload_Failed"].Value} - {ex?.Message}");
|
||||
}
|
||||
|
||||
return Json(new { success = false });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> DownloadGallery(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var gallery = await _database.GetGallery(id);
|
||||
if (gallery != null)
|
||||
{
|
||||
if (await _settings.GetOrDefault(Settings.Gallery.Download, true, gallery?.Name) || (User?.Identity != null && User.Identity.IsAuthenticated))
|
||||
{
|
||||
var galleryDir = id > 0 ? Path.Combine(UploadsDirectory, gallery.Name) : UploadsDirectory;
|
||||
if (_fileHelper.DirectoryExists(galleryDir))
|
||||
{
|
||||
_fileHelper.CreateDirectoryIfNotExists(TempDirectory);
|
||||
|
||||
var tempZipFile = Path.Combine(TempDirectory, $"{gallery.Name}-{DateTime.Now.ToString("yyyy-MM-dd_HH-mm-ss")}.zip");
|
||||
ZipFile.CreateFromDirectory(galleryDir, tempZipFile, CompressionLevel.Optimal, false);
|
||||
|
||||
if (User?.Identity == null || !User.Identity.IsAuthenticated)
|
||||
{
|
||||
using (var fs = new FileStream(tempZipFile, FileMode.Open, FileAccess.ReadWrite))
|
||||
using (var archive = new ZipArchive(fs, ZipArchiveMode.Update, false))
|
||||
{
|
||||
foreach (var entry in archive.Entries.Where(x => x.FullName.StartsWith("Pending/", StringComparison.OrdinalIgnoreCase) || x.FullName.StartsWith("Rejected/", StringComparison.OrdinalIgnoreCase)).ToList())
|
||||
{
|
||||
entry.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Json(new { success = true, filename = $"/temp/{Path.GetFileName(tempZipFile)}" });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(new { success = false, message = _localizer["Download_Gallery_Not_Allowed"].Value });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(new { success = false, message = _localizer["Failed_Download_Gallery"].Value });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{_localizer["Failed_Download_Gallery"].Value} - {ex?.Message}");
|
||||
}
|
||||
|
||||
return Json(new { success = false });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,88 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Models;
|
||||
|
||||
namespace WeddingShare.Controllers
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly IConfigHelper _config;
|
||||
private readonly ISecretKeyHelper _secretKey;
|
||||
private readonly ISettingsHelper _settings;
|
||||
private readonly IDatabaseHelper _database;
|
||||
private readonly IDeviceDetector _deviceDetector;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IStringLocalizer<Lang.Translations> _localizer;
|
||||
|
||||
public HomeController(IConfigHelper config, ISecretKeyHelper secretKey, IDeviceDetector deviceDetector, ILogger<HomeController> logger)
|
||||
public HomeController(ISettingsHelper settings, IDatabaseHelper database, IDeviceDetector deviceDetector, ILogger<HomeController> logger, IStringLocalizer<Lang.Translations> localizer)
|
||||
{
|
||||
_config = config;
|
||||
_secretKey = secretKey;
|
||||
_settings = settings;
|
||||
_database = database;
|
||||
_deviceDetector = deviceDetector;
|
||||
_logger = logger;
|
||||
_localizer = localizer;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var deviceType = HttpContext.Session.GetString("DeviceType");
|
||||
if (string.IsNullOrWhiteSpace(deviceType))
|
||||
{
|
||||
deviceType = (await _deviceDetector.ParseDeviceType(Request.Headers["User-Agent"].ToString())).ToString();
|
||||
HttpContext.Session.SetString("DeviceType", deviceType ?? "Desktop");
|
||||
}
|
||||
var model = new Views.Home.IndexModel();
|
||||
|
||||
if (_config.GetOrDefault("Settings", "Single_Gallery_Mode", false))
|
||||
{
|
||||
var key = await _secretKey.GetGallerySecretKey("default");
|
||||
if (string.IsNullOrEmpty(key))
|
||||
try
|
||||
{
|
||||
var deviceType = HttpContext.Session.GetString(SessionKey.DeviceType);
|
||||
if (string.IsNullOrWhiteSpace(deviceType))
|
||||
{
|
||||
return RedirectToAction("Index", "Gallery");
|
||||
deviceType = (await _deviceDetector.ParseDeviceType(Request.Headers["User-Agent"].ToString())).ToString();
|
||||
HttpContext.Session.SetString(SessionKey.DeviceType, deviceType ?? "Desktop");
|
||||
}
|
||||
|
||||
if (await _settings.GetOrDefault(Settings.Basic.SingleGalleryMode, false))
|
||||
{
|
||||
var key = await _settings.GetOrDefault(Settings.Gallery.SecretKey, string.Empty, "default");
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return RedirectToAction("Index", "Gallery");
|
||||
}
|
||||
}
|
||||
|
||||
model.GalleryNames = await _settings.GetOrDefault(Settings.GallerySelector.Dropdown, false) ? await _database.GetGalleryNames() : new List<string>() { "default" };
|
||||
if (await _settings.GetOrDefault(Settings.GallerySelector.HideDefaultOption, false))
|
||||
{
|
||||
model.GalleryNames = model.GalleryNames.Where(x => !x.Equals("default", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{_localizer["Homepage_Load_Error"].Value} - {ex?.Message}");
|
||||
}
|
||||
|
||||
return View();
|
||||
return View(model);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult SetIdentity(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Regex.IsMatch(name, @"^[a-zA-Z-\s\-\']+$", RegexOptions.Compiled))
|
||||
{
|
||||
HttpContext.Session.SetString(SessionKey.ViewerIdentity, name);
|
||||
|
||||
return Json(new { success = true });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"{_localizer["Identity_Session_Error"].Value}: '{name}'");
|
||||
}
|
||||
|
||||
return Json(new { success = false });
|
||||
}
|
||||
}
|
||||
}
|
||||
65
WeddingShare/Controllers/LanguageController.cs
Normal file
65
WeddingShare/Controllers/LanguageController.cs
Normal file
@@ -0,0 +1,65 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Helpers;
|
||||
using WeddingShare.Models;
|
||||
|
||||
namespace WeddingShare.Controllers
|
||||
{
|
||||
[AllowAnonymous]
|
||||
public class LanguageController : Controller
|
||||
{
|
||||
private readonly ISettingsHelper _settings;
|
||||
private readonly ILanguageHelper _languageHelper;
|
||||
|
||||
public LanguageController(ISettingsHelper settings, ILanguageHelper languageHelper)
|
||||
{
|
||||
_settings = settings;
|
||||
_languageHelper = languageHelper;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var options = new List<SupportedLanguage>();
|
||||
|
||||
try
|
||||
{
|
||||
var defaultLang = HttpContext.Session.GetString(SessionKey.SelectedLanguage);
|
||||
if (string.IsNullOrWhiteSpace(defaultLang))
|
||||
{
|
||||
defaultLang = await _settings.GetOrDefault(Settings.Languages.Default, "en-GB");
|
||||
}
|
||||
|
||||
options = (await _languageHelper.DetectSupportedCulturesAsync())
|
||||
.Where(x => x.Name.Contains("-"))
|
||||
.Select(x => new SupportedLanguage() { Key = x.Name, Value = $"{(x.EnglishName.Contains("(") ? x.EnglishName.Substring(0, x.EnglishName.IndexOf("(")) : x.EnglishName).Trim()} ({x.Name})", Selected = string.Equals(defaultLang, x.Name, StringComparison.OrdinalIgnoreCase) })
|
||||
.OrderBy(x => x.Value.ToLower())
|
||||
.ToList();
|
||||
}
|
||||
catch { }
|
||||
|
||||
return Json(new { supported = options });
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> ChangeDisplayLanguage(string culture)
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpContext.Session.SetString(SessionKey.SelectedLanguage, culture);
|
||||
Response.Cookies.Append(
|
||||
CookieRequestCultureProvider.DefaultCookieName,
|
||||
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
|
||||
new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
|
||||
);
|
||||
|
||||
return Json(new { success = true });
|
||||
}
|
||||
catch { }
|
||||
|
||||
return Json(new { success = false });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,19 @@ FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
|
||||
WORKDIR /app
|
||||
EXPOSE 5000
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG TARGETARCH
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
WORKDIR /src
|
||||
COPY ["WeddingShare/WeddingShare.csproj", "WeddingShare/"]
|
||||
RUN dotnet restore "./WeddingShare/./WeddingShare.csproj"
|
||||
RUN dotnet restore -a $TARGETARCH "./WeddingShare/./WeddingShare.csproj"
|
||||
COPY . .
|
||||
WORKDIR "/src/WeddingShare"
|
||||
RUN dotnet build "./WeddingShare.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
RUN dotnet build "./WeddingShare.csproj" -a $TARGETARCH -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./WeddingShare.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
RUN dotnet publish "./WeddingShare.csproj" -a $TARGETARCH -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
public enum DatabaseType
|
||||
{
|
||||
Unknown,
|
||||
SQLite
|
||||
SQLite,
|
||||
MySQL
|
||||
}
|
||||
}
|
||||
12
WeddingShare/Enums/GalleryFilter.cs
Normal file
12
WeddingShare/Enums/GalleryFilter.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace WeddingShare.Enums
|
||||
{
|
||||
public enum GalleryFilter
|
||||
{
|
||||
All,
|
||||
Images,
|
||||
Videos,
|
||||
Landscape,
|
||||
Portrait,
|
||||
Square
|
||||
}
|
||||
}
|
||||
10
WeddingShare/Enums/GalleryGroup.cs
Normal file
10
WeddingShare/Enums/GalleryGroup.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace WeddingShare.Enums
|
||||
{
|
||||
public enum GalleryGroup
|
||||
{
|
||||
None,
|
||||
Date,
|
||||
MediaType,
|
||||
Uploader
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,8 @@
|
||||
{
|
||||
public enum GalleryOrder
|
||||
{
|
||||
None,
|
||||
UploadedAsc,
|
||||
UploadedDesc,
|
||||
NameAsc,
|
||||
NameDesc
|
||||
Ascending,
|
||||
Descending,
|
||||
Random
|
||||
}
|
||||
}
|
||||
10
WeddingShare/Enums/MediaType.cs
Normal file
10
WeddingShare/Enums/MediaType.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace WeddingShare.Enums
|
||||
{
|
||||
public enum MediaType
|
||||
{
|
||||
Unknown,
|
||||
Image,
|
||||
Video,
|
||||
All
|
||||
}
|
||||
}
|
||||
8
WeddingShare/Enums/Themes.cs
Normal file
8
WeddingShare/Enums/Themes.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace WeddingShare.Enums
|
||||
{
|
||||
public enum Themes
|
||||
{
|
||||
Default = 1,
|
||||
Dark = 2
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
public enum ViewMode
|
||||
{
|
||||
Default,
|
||||
Presentation
|
||||
Presentation,
|
||||
Slideshow
|
||||
}
|
||||
}
|
||||
19
WeddingShare/Extensions/DictionaryExtensions.cs
Normal file
19
WeddingShare/Extensions/DictionaryExtensions.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace WeddingShare.Extensions
|
||||
{
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
public static string GetValue(this IDictionary<string, string> value, string key, string defaultValue = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
if (value.ContainsKey(key))
|
||||
{
|
||||
return value[key];
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,16 @@
|
||||
{
|
||||
public interface IConfigHelper
|
||||
{
|
||||
string? GetEnvironmentVariable(string key);
|
||||
string? GetConfigValue(string section, string key);
|
||||
string? Get(string section, string key);
|
||||
string GetOrDefault(string section, string key, string defaultValue);
|
||||
int GetOrDefault(string section, string key, int defaultValue);
|
||||
long GetOrDefault(string section, string key, long defaultValue);
|
||||
decimal GetOrDefault(string section, string key, decimal defaultValue);
|
||||
double GetOrDefault(string section, string key, double defaultValue);
|
||||
bool GetOrDefault(string section, string key, bool defaultValue);
|
||||
DateTime? GetOrDefault(string section, string key, DateTime? defaultValue);
|
||||
string? GetEnvironmentVariable(string key, string? galleryId = null);
|
||||
string? GetConfigValue(string key);
|
||||
string? Get(string key, string? galleryId = null);
|
||||
string GetOrDefault(string key, string defaultValue);
|
||||
int GetOrDefault(string key, int defaultValue);
|
||||
long GetOrDefault(string key, long defaultValue);
|
||||
decimal GetOrDefault(string key, decimal defaultValue);
|
||||
double GetOrDefault(string key, double defaultValue);
|
||||
bool GetOrDefault(string key, bool defaultValue);
|
||||
DateTime? GetOrDefault(string key, DateTime? defaultValue);
|
||||
}
|
||||
|
||||
public class ConfigHelper : IConfigHelper
|
||||
@@ -27,30 +27,35 @@
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public string? GetEnvironmentVariable(string key)
|
||||
public string? GetEnvironmentVariable(string key, string? galleryId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = _environment.GetEnvironmentVariable(key.Replace(":", "_").ToUpper());
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
var envKey = !string.IsNullOrWhiteSpace(galleryId) ? $"{key}_{galleryId}" : key;
|
||||
if (!this.IsProtectedVariable(envKey))
|
||||
{
|
||||
return value;
|
||||
var keyName = string.Join('_', envKey.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Skip(1)).Trim('_').ToUpper();
|
||||
|
||||
var value = _environment.GetEnvironmentVariable(keyName);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"Failed to get environment variable '{key}'");
|
||||
_logger.LogWarning(ex, $"Failed to get environment variable '{key}' for gallery '{galleryId}'");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? GetConfigValue(string section, string key)
|
||||
public string? GetConfigValue(string key)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configKey = !string.IsNullOrWhiteSpace(section) ? $"{section}:{key}" : key;
|
||||
var value = _configuration.GetValue<string>(configKey);
|
||||
var value = _configuration.GetValue<string>(key);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
@@ -64,20 +69,26 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
public string? Get(string section, string key)
|
||||
public string? Get(string key, string? galleryId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = !IsProtectedVariable($"{section}_{key}") ? this.GetEnvironmentVariable(key) : string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
var envValue = this.GetEnvironmentVariable(key, galleryId);
|
||||
if (!string.IsNullOrWhiteSpace(envValue))
|
||||
{
|
||||
return value;
|
||||
return envValue;
|
||||
}
|
||||
|
||||
value = this.GetConfigValue(section, key);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
envValue = this.GetEnvironmentVariable(key);
|
||||
if (!string.IsNullOrWhiteSpace(envValue))
|
||||
{
|
||||
return value;
|
||||
return envValue;
|
||||
}
|
||||
|
||||
var configValue = this.GetConfigValue(key);
|
||||
if (!string.IsNullOrWhiteSpace(configValue))
|
||||
{
|
||||
return configValue;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -88,11 +99,11 @@
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetOrDefault(string section, string key, string defaultValue)
|
||||
public string GetOrDefault(string key, string defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = this.Get(section, key);
|
||||
var value = this.Get(key);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
@@ -103,11 +114,11 @@
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public int GetOrDefault(string section, string key, int defaultValue)
|
||||
public int GetOrDefault(string key, int defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = this.GetOrDefault(section, key, string.Empty);
|
||||
var value = this.GetOrDefault(key, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToInt32(value);
|
||||
@@ -118,11 +129,11 @@
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public long GetOrDefault(string section, string key, long defaultValue)
|
||||
public long GetOrDefault(string key, long defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = this.GetOrDefault(section, key, string.Empty);
|
||||
var value = this.GetOrDefault(key, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToInt64(value);
|
||||
@@ -133,11 +144,11 @@
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public decimal GetOrDefault(string section, string key, decimal defaultValue)
|
||||
public decimal GetOrDefault(string key, decimal defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = this.GetOrDefault(section, key, string.Empty);
|
||||
var value = this.GetOrDefault(key, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToDecimal(value);
|
||||
@@ -148,11 +159,11 @@
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public double GetOrDefault(string section, string key, double defaultValue)
|
||||
public double GetOrDefault(string key, double defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = this.GetOrDefault(section, key, string.Empty);
|
||||
var value = this.GetOrDefault(key, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToDouble(value);
|
||||
@@ -163,11 +174,11 @@
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public bool GetOrDefault(string section, string key, bool defaultValue)
|
||||
public bool GetOrDefault(string key, bool defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = this.GetOrDefault(section, key, string.Empty);
|
||||
var value = this.GetOrDefault(key, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToBoolean(value);
|
||||
@@ -178,11 +189,11 @@
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public DateTime? GetOrDefault(string section, string key, DateTime? defaultValue)
|
||||
public DateTime? GetOrDefault(string key, DateTime? defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = this.GetOrDefault(section, key, string.Empty);
|
||||
var value = this.GetOrDefault(key, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToDateTime(value);
|
||||
|
||||
@@ -5,7 +5,11 @@ namespace WeddingShare.Helpers.Database
|
||||
{
|
||||
public interface IDatabaseHelper
|
||||
{
|
||||
Task<int> GetGalleryCount();
|
||||
Task<IEnumerable<string>> GetGalleryNames();
|
||||
Task<List<GalleryModel>> GetAllGalleries();
|
||||
Task<int?> GetGalleryId(string name);
|
||||
Task<string?> GetGalleryName(int id);
|
||||
Task<GalleryModel?> GetGallery(int id);
|
||||
Task<GalleryModel?> GetGallery(string name);
|
||||
Task<GalleryModel?> AddGallery(GalleryModel model);
|
||||
@@ -13,15 +17,39 @@ namespace WeddingShare.Helpers.Database
|
||||
Task<bool> WipeGallery(GalleryModel model);
|
||||
Task<bool> WipeAllGalleries();
|
||||
Task<bool> DeleteGallery(GalleryModel model);
|
||||
Task<List<GalleryItemModel>> GetAllGalleryItems(int galleryId, GalleryItemState state = GalleryItemState.All);
|
||||
Task<IDictionary<string, long>> GetGalleryItemCount(int? galleryId, GalleryItemState state = GalleryItemState.All, MediaType type = MediaType.All, ImageOrientation orientation = ImageOrientation.None);
|
||||
Task<List<GalleryItemModel>> GetAllGalleryItems(int? galleryId, GalleryItemState state = GalleryItemState.All, MediaType type = MediaType.All, ImageOrientation orientation = ImageOrientation.None, GalleryGroup group = GalleryGroup.None, GalleryOrder order = GalleryOrder.Descending, int limit = int.MaxValue, int page = 1);
|
||||
Task<int> GetPendingGalleryItemCount(int? galleryId = null);
|
||||
Task<List<PendingGalleryItemModel>> GetPendingGalleryItems(int? galleryId = null);
|
||||
Task<PendingGalleryItemModel?> GetPendingGalleryItem(int id);
|
||||
Task<List<GalleryItemModel>> GetPendingGalleryItems(int? galleryId = null);
|
||||
Task<GalleryItemModel?> GetPendingGalleryItem(int id);
|
||||
Task<GalleryItemModel?> GetGalleryItem(int id);
|
||||
Task<GalleryItemModel?> GetGalleryItemByChecksum(int galleryId, string checksum);
|
||||
Task<GalleryItemModel?> AddGalleryItem(GalleryItemModel model);
|
||||
Task<GalleryItemModel?> EditGalleryItem(GalleryItemModel model);
|
||||
Task<bool> DeleteGalleryItem(GalleryItemModel model);
|
||||
Task<bool> InitAdminAccount(UserModel model);
|
||||
Task<bool> ValidateCredentials(string username, string password);
|
||||
Task<List<UserModel>?> GetAllUsers();
|
||||
Task<UserModel?> GetUser(int id);
|
||||
Task<UserModel?> GetUser(string name);
|
||||
Task<UserModel?> AddUser(UserModel model);
|
||||
Task<UserModel?> EditUser(UserModel model);
|
||||
Task<bool> DeleteUser(UserModel model);
|
||||
Task<bool> ChangePassword(UserModel model);
|
||||
Task<int> IncrementLockoutCount(int id);
|
||||
Task<bool> SetLockout(int id, DateTime? datetime);
|
||||
Task<bool> ResetLockoutCount(int id);
|
||||
Task<bool> SetMultiFactorToken(int id, string token);
|
||||
Task<bool> ResetMultiFactorToDefault();
|
||||
Task<bool> Import(string path);
|
||||
Task<bool> Export(string path);
|
||||
Task<IEnumerable<SettingModel>?> GetAllSettings(string? gallery = "");
|
||||
Task<SettingModel?> GetSetting(string id, string? gallery = "");
|
||||
Task<SettingModel?> GetGallerySpecificSetting(string id, string gallery);
|
||||
Task<SettingModel?> AddSetting(SettingModel model, string? gallery = "");
|
||||
Task<SettingModel?> EditSetting(SettingModel model, string? gallery = "");
|
||||
Task<SettingModel?> SetSetting(SettingModel model, string? gallery = "");
|
||||
Task<bool> DeleteSetting(SettingModel model, string? gallery = "");
|
||||
Task<bool> DeleteAllSettings(string? gallery = "");
|
||||
}
|
||||
}
|
||||
1595
WeddingShare/Helpers/Database/MySqlDatabaseHelper.cs
Normal file
1595
WeddingShare/Helpers/Database/MySqlDatabaseHelper.cs
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,52 +1,80 @@
|
||||
using System.Reflection;
|
||||
using DbUp;
|
||||
using DbUp.Engine;
|
||||
using WeddingShare.Constants;
|
||||
using WeddingShare.Enums;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Models.Database;
|
||||
|
||||
namespace WeddingShare.Helpers.Dbup
|
||||
{
|
||||
public sealed class DbupMigrator(IEnvironmentWrapper environment, IConfiguration configuration, ILoggerFactory loggerFactory) : BackgroundService
|
||||
public sealed class DbupMigrator(IEnvironmentWrapper environment, IConfiguration configuration, IDatabaseHelper database, IFileHelper fileHelper, IEncryptionHelper encryption, ILoggerFactory loggerFactory) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
var logger = loggerFactory.CreateLogger<DbupMigrator>();
|
||||
|
||||
fileHelper.CreateDirectoryIfNotExists("config");
|
||||
|
||||
var config = new ConfigHelper(environment, configuration, loggerFactory.CreateLogger<ConfigHelper>());
|
||||
|
||||
var connString = config.GetOrDefault(Settings.Database.ConnectionString, "Data Source=./config/wedding-share.db");
|
||||
if (!string.IsNullOrWhiteSpace(connString))
|
||||
{
|
||||
var logger = loggerFactory.CreateLogger<DbupMigrator>();
|
||||
DatabaseUpgradeResult? dbupResult;
|
||||
|
||||
if (!Directory.Exists("config"))
|
||||
var dbType = config.GetOrDefault(Settings.Database.Type, "sqlite")?.ToLower();
|
||||
switch (dbType)
|
||||
{
|
||||
case "sqlite":
|
||||
dbupResult = new DbupSqliteHelper().Migrate(connString);
|
||||
break;
|
||||
case "mysql":
|
||||
dbupResult = new DbupMySqlHelper().Migrate(connString);
|
||||
break;
|
||||
default:
|
||||
var error = $"Database type '{dbType}' is not yet supported by this application";
|
||||
logger.LogWarning(error);
|
||||
throw new NotImplementedException(error);
|
||||
}
|
||||
|
||||
if (dbupResult != null && !dbupResult.Successful)
|
||||
{
|
||||
logger.LogWarning($"DBUP failed with error: '{dbupResult?.Error?.Message}' - '{dbupResult?.Error?.ToString()}'");
|
||||
}
|
||||
|
||||
if (config.GetOrDefault(Settings.Database.SyncFromConfig, false))
|
||||
{
|
||||
Directory.CreateDirectory("config");
|
||||
logger.LogWarning($"Sync_From_Config set to true, wiping settings database and re-pulling values from config");
|
||||
await database.DeleteAllSettings();
|
||||
}
|
||||
|
||||
var config = new ConfigHelper(environment, configuration, loggerFactory.CreateLogger<ConfigHelper>());
|
||||
var connString = config.GetOrDefault("Database", "Connection_String", "Data Source=./config/wedding-share.db");
|
||||
if (!string.IsNullOrWhiteSpace(connString))
|
||||
var isDemoMode = config.GetOrDefault(Settings.IsDemoMode, false);
|
||||
var username = !isDemoMode ? config.GetOrDefault(Settings.Account.Admin.Username, "admin").ToLower() : "demo";
|
||||
var adminAccount = new UserModel()
|
||||
{
|
||||
DatabaseUpgradeResult? dbupResult;
|
||||
Username = username,
|
||||
Password = encryption.Encrypt(!isDemoMode ? config.GetOrDefault(Settings.Account.Admin.Password, "admin") : "demo", username)
|
||||
};
|
||||
await database.InitAdminAccount(adminAccount);
|
||||
|
||||
var dbType = config.GetOrDefault("Database", "Database_Type", "sqlite")?.ToLower();
|
||||
switch (dbType)
|
||||
{
|
||||
case "sqlite":
|
||||
dbupResult = new DbupSqliteHelper().Migrate(connString);
|
||||
break;
|
||||
default:
|
||||
var error = $"Database type '{dbType}' is not yet supported by this application";
|
||||
logger.LogWarning(error);
|
||||
throw new NotImplementedException(error);
|
||||
}
|
||||
await new DbupImporter(config, database, loggerFactory.CreateLogger<DbupImporter>()).ImportSettings();
|
||||
|
||||
if (dbupResult != null && !dbupResult.Successful)
|
||||
{
|
||||
logger.LogWarning($"DBUP failed with error: '{dbupResult?.Error?.Message}' - '{dbupResult?.Error?.ToString()}'");
|
||||
}
|
||||
}
|
||||
else
|
||||
if (config.GetOrDefault(Settings.Account.Admin.LogPassword, false))
|
||||
{
|
||||
logger.LogError($"DBUP failed with error: 'Connection string was null or empty'");
|
||||
throw new ArgumentNullException("Please specify a valid database connection string");
|
||||
logger.LogInformation($"Password: {adminAccount.Password}");
|
||||
}
|
||||
}, stoppingToken);
|
||||
|
||||
if (config.GetOrDefault(Security.MultiFactor.ResetToDefault, false))
|
||||
{
|
||||
await database.ResetMultiFactorToDefault();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError($"DBUP failed with error: 'Connection string was null or empty'");
|
||||
throw new ArgumentNullException("Please specify a valid database connection string");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +85,35 @@ namespace WeddingShare.Helpers.Dbup
|
||||
try
|
||||
{
|
||||
var dbupBuilder = DeployChanges.To
|
||||
.SQLiteDatabase(connectionString)
|
||||
.SqliteDatabase(connectionString)
|
||||
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
|
||||
.WithScriptNameComparer(new DbupScriptComparer())
|
||||
.WithFilter(new DbupScriptFilter(DatabaseType.SQLite))
|
||||
.LogToConsole();
|
||||
dbupBuilder.Configure(c => c.Journal = new DbupTableJournal(() => c.ConnectionManager, () => c.Log, "schemaversions"));
|
||||
dbupBuilder.Configure(c => c.Journal = new DbupSQLiteTableJournal(() => c.ConnectionManager, () => c.Log, "schemaversions"));
|
||||
|
||||
return dbupBuilder.Build().PerformUpgrade();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new DatabaseUpgradeResult(null, false, ex, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class DbupMySqlHelper
|
||||
{
|
||||
public DatabaseUpgradeResult Migrate(string connectionString)
|
||||
{
|
||||
try
|
||||
{
|
||||
var dbupBuilder = DeployChanges.To
|
||||
.MySqlDatabase(connectionString)
|
||||
.WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly())
|
||||
.WithScriptNameComparer(new DbupScriptComparer())
|
||||
.WithFilter(new DbupScriptFilter(DatabaseType.MySQL))
|
||||
.LogToConsole();
|
||||
dbupBuilder.Configure(c => c.Journal = new DbupMySqlTableJournal(() => c.ConnectionManager, () => c.Log, "weddingshare", "schemaversions"));
|
||||
|
||||
return dbupBuilder.Build().PerformUpgrade();
|
||||
}
|
||||
|
||||
136
WeddingShare/Helpers/Dbup/DbupImporter.cs
Normal file
136
WeddingShare/Helpers/Dbup/DbupImporter.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Collections;
|
||||
using System.Reflection;
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Models.Database;
|
||||
|
||||
namespace WeddingShare.Helpers.Dbup
|
||||
{
|
||||
public class DbupImporter(IConfigHelper config, IDatabaseHelper database, ILogger<DbupImporter> logger)
|
||||
{
|
||||
public async Task ImportSettings()
|
||||
{
|
||||
try
|
||||
{
|
||||
var settings = await database.GetAllSettings();
|
||||
if (settings == null || !settings.Any())
|
||||
{
|
||||
var systemKeys = GetAllKeys();
|
||||
foreach (var key in systemKeys)
|
||||
{
|
||||
try
|
||||
{
|
||||
var configVal = config.Get(key);
|
||||
if (!string.IsNullOrWhiteSpace(configVal))
|
||||
{
|
||||
await database.AddSetting(new SettingModel()
|
||||
{
|
||||
Id = key,
|
||||
Value = configVal
|
||||
});
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
var galleries = await database.GetAllGalleries();
|
||||
if (galleries != null && galleries.Any())
|
||||
{
|
||||
var galleryKeys = GetKeys<Constants.Settings.Gallery>();
|
||||
foreach (var gallery in galleries)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(gallery?.Name))
|
||||
{
|
||||
foreach (var key in galleryKeys)
|
||||
{
|
||||
try
|
||||
{
|
||||
var galleryOverride = config.GetEnvironmentVariable(key, gallery.Name);
|
||||
if (!string.IsNullOrWhiteSpace(galleryOverride))
|
||||
{
|
||||
await database.AddSetting(new SettingModel()
|
||||
{
|
||||
Id = key,
|
||||
Value = galleryOverride
|
||||
}, gallery.Name);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError($"Failed to import settings at startup - {ex?.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetAllKeys()
|
||||
{
|
||||
var keys = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
keys.AddRange(GetKeys<Constants.BackgroundServices>());
|
||||
keys.AddRange(GetKeys<Constants.Notifications>());
|
||||
keys.AddRange(GetKeys<Constants.Security>());
|
||||
keys.AddRange(GetKeys<Constants.Settings>());
|
||||
}
|
||||
catch { }
|
||||
|
||||
return keys.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct();
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetKeys<T>(bool includeNesteted = true)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
var obj = Activator.CreateInstance<T>();
|
||||
foreach (var val in GetConstants(typeof(T), includeNesteted))
|
||||
{
|
||||
keys.Add((string)(val.GetValue(obj) ?? string.Empty));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return keys.Where(x => !string.IsNullOrWhiteSpace(x));
|
||||
}
|
||||
|
||||
private FieldInfo[] GetConstants(Type type, bool includeNesteted)
|
||||
{
|
||||
var constants = new ArrayList();
|
||||
|
||||
try
|
||||
{
|
||||
if (includeNesteted)
|
||||
{
|
||||
var classInfos = type.GetNestedTypes(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
|
||||
foreach (var ci in classInfos)
|
||||
{
|
||||
var consts = GetConstants(ci, includeNesteted);
|
||||
if (consts != null && consts.Length > 0)
|
||||
{
|
||||
constants.AddRange(consts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var fieldInfos = type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
|
||||
foreach (var fi in fieldInfos)
|
||||
{
|
||||
if (fi.IsLiteral && !fi.IsInitOnly)
|
||||
{
|
||||
constants.Add(fi);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return (FieldInfo[])constants.ToArray(typeof(FieldInfo));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
WeddingShare/Helpers/Dbup/DbupMySqlTableJournal.cs
Normal file
33
WeddingShare/Helpers/Dbup/DbupMySqlTableJournal.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Data;
|
||||
using DbUp.Engine;
|
||||
using DbUp.Engine.Output;
|
||||
using DbUp.Engine.Transactions;
|
||||
using DbUp.MySql;
|
||||
|
||||
namespace WeddingShare.Helpers.Dbup
|
||||
{
|
||||
public class DbupMySqlTableJournal : MySqlTableJournal
|
||||
{
|
||||
public DbupMySqlTableJournal(Func<IConnectionManager> connectionManager, Func<IUpgradeLog> logger, string scheme, string table)
|
||||
: base(connectionManager, logger, scheme, table)
|
||||
{
|
||||
}
|
||||
|
||||
public override void StoreExecutedScript(SqlScript script, Func<IDbCommand> dbCommandFactory)
|
||||
{
|
||||
var scriptName = script.Name;
|
||||
|
||||
try
|
||||
{
|
||||
var parts = script.Name.Split('.', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts != null && parts.Length >= 2)
|
||||
{
|
||||
scriptName = string.Join(".", parts.Skip(parts.Length - 2).Take(2));
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
base.StoreExecutedScript(new SqlScript(scriptName, script.Contents), dbCommandFactory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,13 @@
|
||||
using DbUp.Engine;
|
||||
using DbUp.Engine.Output;
|
||||
using DbUp.Engine.Transactions;
|
||||
using DbUp.SQLite;
|
||||
using DbUp.Sqlite;
|
||||
|
||||
namespace WeddingShare.Helpers.Dbup
|
||||
{
|
||||
public class DbupTableJournal : SQLiteTableJournal
|
||||
public class DbupSQLiteTableJournal : SqliteTableJournal
|
||||
{
|
||||
public DbupTableJournal(Func<IConnectionManager> connectionManager, Func<IUpgradeLog> logger, string table)
|
||||
public DbupSQLiteTableJournal(Func<IConnectionManager> connectionManager, Func<IUpgradeLog> logger, string table)
|
||||
: base(connectionManager, logger, table)
|
||||
{
|
||||
}
|
||||
@@ -20,6 +20,8 @@ namespace WeddingShare.Helpers.Dbup
|
||||
{
|
||||
case DatabaseType.SQLite:
|
||||
return scripts.Where(s => s.Name.ToLower().Contains(".sqlscripts.sqlite."));
|
||||
case DatabaseType.MySQL:
|
||||
return scripts.Where(s => s.Name.ToLower().Contains(".sqlscripts.mysql."));
|
||||
default:
|
||||
return new List<SqlScript>();
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace WeddingShare.Helpers
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(userAgent))
|
||||
if (string.IsNullOrWhiteSpace(userAgent))
|
||||
{
|
||||
return DeviceType.Unknown;
|
||||
}
|
||||
|
||||
90
WeddingShare/Helpers/EncryptionHelper.cs
Normal file
90
WeddingShare/Helpers/EncryptionHelper.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using WeddingShare.Constants;
|
||||
|
||||
namespace WeddingShare.Helpers
|
||||
{
|
||||
public interface IEncryptionHelper
|
||||
{
|
||||
bool IsEncryptionEnabled();
|
||||
string Encrypt(string value, string? salt = null);
|
||||
}
|
||||
|
||||
public class EncryptionHelper : IEncryptionHelper
|
||||
{
|
||||
private readonly HashAlgorithmName _hashType;
|
||||
private readonly int _iterations;
|
||||
private readonly string _key;
|
||||
private readonly string _salt;
|
||||
|
||||
public EncryptionHelper(ISettingsHelper settings)
|
||||
{
|
||||
_hashType = ParseHashType(settings.GetOrDefault(Security.Encryption.HashType, "SHA256").Result);
|
||||
_iterations = settings.GetOrDefault(Security.Encryption.Iterations, 1000).Result;
|
||||
|
||||
_key = settings.GetOrDefault(Security.Encryption.Key, string.Empty).Result;
|
||||
_salt = settings.GetOrDefault(Security.Encryption.Salt, "WUtlVOvC2a6ol9M6ZidO5sJkQxYMolyasFid2Fyqvjd0uucAjYy5EsHPxdeplFRj").Result;
|
||||
}
|
||||
|
||||
public bool IsEncryptionEnabled()
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(_key) && !string.IsNullOrWhiteSpace(_salt);
|
||||
}
|
||||
|
||||
public string Encrypt(string value, string? salt = null)
|
||||
{
|
||||
var enabled = this.IsEncryptionEnabled();
|
||||
if (enabled)
|
||||
{
|
||||
var clearBytes = Encoding.Unicode.GetBytes(value);
|
||||
var saltBytes = Encoding.Unicode.GetBytes(salt ?? _salt);
|
||||
|
||||
using (var encryptor = Aes.Create())
|
||||
{
|
||||
var pdb = new Rfc2898DeriveBytes(_key, saltBytes, _iterations, _hashType);
|
||||
|
||||
encryptor.Key = pdb.GetBytes(32);
|
||||
encryptor.IV = pdb.GetBytes(16);
|
||||
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
using (var cs = new CryptoStream(ms, encryptor.CreateEncryptor(), CryptoStreamMode.Write))
|
||||
{
|
||||
cs.Write(clearBytes, 0, clearBytes.Length);
|
||||
cs.Close();
|
||||
}
|
||||
|
||||
value = Convert.ToBase64String(ms.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private HashAlgorithmName ParseHashType(string name)
|
||||
{
|
||||
switch (name?.Trim()?.ToUpper())
|
||||
{
|
||||
case "MD5":
|
||||
return HashAlgorithmName.MD5;
|
||||
case "SHA1":
|
||||
return HashAlgorithmName.SHA1;
|
||||
case "SHA256":
|
||||
return HashAlgorithmName.SHA256;
|
||||
case "SHA384":
|
||||
return HashAlgorithmName.SHA384;
|
||||
case "SHA512":
|
||||
return HashAlgorithmName.SHA512;
|
||||
case "SHA3_256":
|
||||
return HashAlgorithmName.SHA3_256;
|
||||
case "SHA3_384":
|
||||
return HashAlgorithmName.SHA3_384;
|
||||
case "SHA3_512":
|
||||
return HashAlgorithmName.SHA3_512;
|
||||
default:
|
||||
return HashAlgorithmName.SHA256;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,5 +6,7 @@
|
||||
public const int Unauthorized = 401;
|
||||
public const int InvalidSecretKey = 402;
|
||||
public const int GalleryCreationNotAllowed = 403;
|
||||
public const int GalleryLimitReached = 405;
|
||||
public const int InvalidGalleryId = 406;
|
||||
}
|
||||
}
|
||||
215
WeddingShare/Helpers/FileHelper.cs
Normal file
215
WeddingShare/Helpers/FileHelper.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System.Numerics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace WeddingShare.Helpers
|
||||
{
|
||||
public interface IFileHelper
|
||||
{
|
||||
bool DirectoryExists(string path);
|
||||
bool CreateDirectoryIfNotExists(string path);
|
||||
bool DeleteDirectoryIfExists(string path, bool recursive = true);
|
||||
bool PurgeDirectory(string path);
|
||||
string[] GetDirectories(string path, string pattern = "*", SearchOption searchOption = SearchOption.AllDirectories);
|
||||
string[] GetFiles(string path, string pattern = "*.*", SearchOption searchOption = SearchOption.AllDirectories);
|
||||
bool FileExists(string path);
|
||||
long FileSize(string path);
|
||||
bool DeleteFileIfExists(string path);
|
||||
bool MoveFileIfExists(string source, string destination);
|
||||
long GetDirectorySize(string path);
|
||||
Task<byte[]> ReadAllBytes(string path);
|
||||
Task SaveFile(IFormFile file, string path, FileMode mode);
|
||||
Task<string> GetChecksum(string path);
|
||||
Task<DateTime?> GetCreationDatetime(string path);
|
||||
string BytesToHumanReadable(long bytes, int decimalPlaces = 0);
|
||||
}
|
||||
|
||||
public class FileHelper : IFileHelper
|
||||
{
|
||||
private readonly ILogger<FileHelper> _logger;
|
||||
|
||||
public FileHelper(ILogger<FileHelper> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool DirectoryExists(string path)
|
||||
{
|
||||
return Directory.Exists(path);
|
||||
}
|
||||
|
||||
public bool CreateDirectoryIfNotExists(string path)
|
||||
{
|
||||
if (!DirectoryExists(path))
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool DeleteDirectoryIfExists(string path, bool recursive = true)
|
||||
{
|
||||
if (DirectoryExists(path))
|
||||
{
|
||||
Directory.Delete(path, recursive);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool PurgeDirectory(string path)
|
||||
{
|
||||
DeleteDirectoryIfExists(path);
|
||||
return CreateDirectoryIfNotExists(path);
|
||||
}
|
||||
|
||||
public string[] GetDirectories(string path, string pattern = "*", SearchOption searchOption = SearchOption.AllDirectories)
|
||||
{
|
||||
return Directory.GetDirectories(path, pattern, searchOption);
|
||||
}
|
||||
|
||||
public string[] GetFiles(string path, string pattern = "*", SearchOption searchOption = SearchOption.AllDirectories)
|
||||
{
|
||||
return Directory.GetFiles(path, pattern, searchOption);
|
||||
}
|
||||
|
||||
public bool FileExists(string path)
|
||||
{
|
||||
return File.Exists(path);
|
||||
}
|
||||
|
||||
public long FileSize(string path)
|
||||
{
|
||||
return new FileInfo(path).Length;
|
||||
}
|
||||
|
||||
public bool DeleteFileIfExists(string path)
|
||||
{
|
||||
if (FileExists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool MoveFileIfExists(string source, string destination)
|
||||
{
|
||||
if (FileExists(source))
|
||||
{
|
||||
File.Move(source, destination);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public long GetDirectorySize(string path)
|
||||
{
|
||||
long size = 0;
|
||||
|
||||
if (DirectoryExists(path))
|
||||
{
|
||||
var info = new DirectoryInfo(path);
|
||||
|
||||
foreach (var file in info.GetFiles())
|
||||
{
|
||||
size += file.Length;
|
||||
}
|
||||
|
||||
foreach (var dir in info.GetDirectories())
|
||||
{
|
||||
size += GetDirectorySize(dir.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
public async Task<byte[]> ReadAllBytes(string path)
|
||||
{
|
||||
return await File.ReadAllBytesAsync(path);
|
||||
}
|
||||
|
||||
public async Task SaveFile(IFormFile file, string path, FileMode mode)
|
||||
{
|
||||
using (var fs = new FileStream(path, mode))
|
||||
{
|
||||
await file.CopyToAsync(fs);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> GetChecksum(string path)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var checksum = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
using (var md5 = MD5.Create())
|
||||
using (var stream = File.OpenRead(path))
|
||||
{
|
||||
checksum = Encoding.UTF8.GetString(md5.ComputeHash(stream));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"Failed to compute MD5 checksum for file '{path}'");
|
||||
}
|
||||
|
||||
return checksum;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<DateTime?> GetCreationDatetime(string path)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return new FileInfo(path).CreationTimeUtc;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public string BytesToHumanReadable(long bytes, int decimalPlaces = 0)
|
||||
{
|
||||
var sizes = new string[] { "B", "KB", "MB", "GB", "TB", "PB", "EB" };
|
||||
var place = 0;
|
||||
var total = 0.0;
|
||||
|
||||
var decimalFormat = "###0.";
|
||||
for (var i = 0; i < decimalPlaces; i++)
|
||||
{
|
||||
decimalFormat += "0";
|
||||
}
|
||||
|
||||
if (bytes >= 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
long b = Math.Abs(bytes);
|
||||
place = Convert.ToInt32(Math.Floor(Math.Log(b ,1000)));
|
||||
double num = Math.Round(b / Math.Pow(1000, place), 2);
|
||||
total = Math.Sign(bytes) * num;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return total.ToString($"{decimalFormat.TrimEnd('.')} {sizes[place]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,75 +1,150 @@
|
||||
using SixLabors.ImageSharp;
|
||||
using Microsoft.AspNetCore.StaticFiles;
|
||||
using Microsoft.Extensions.Localization;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using WeddingShare.Enums;
|
||||
using Xabe.FFmpeg;
|
||||
using Xabe.FFmpeg.Downloader;
|
||||
|
||||
namespace WeddingShare.Helpers
|
||||
{
|
||||
public interface IImageHelper
|
||||
{
|
||||
Task<bool> GenerateThumbnail(string imagePath, string savePath, int size = 720);
|
||||
Task<bool> GenerateThumbnail(string filePath, string savePath, int size = 720);
|
||||
Task<ImageOrientation> GetOrientation(string path);
|
||||
ImageOrientation GetOrientation(Image img);
|
||||
MediaType GetMediaType(string filePath);
|
||||
Task<bool> DownloadFFMPEG(string path);
|
||||
}
|
||||
|
||||
public class ImageHelper : IImageHelper
|
||||
{
|
||||
private readonly IFileHelper _fileHelper;
|
||||
private readonly ILogger _logger;
|
||||
private readonly IStringLocalizer<Lang.Translations> _localizer;
|
||||
|
||||
public ImageHelper(ILogger<ImageHelper> logger)
|
||||
private static bool FfmpegInstalled = false;
|
||||
|
||||
public ImageHelper(IFileHelper fileHelper, ILogger<ImageHelper> logger, IStringLocalizer<Lang.Translations> localizer)
|
||||
{
|
||||
_fileHelper = fileHelper;
|
||||
_logger = logger;
|
||||
_localizer = localizer;
|
||||
}
|
||||
|
||||
public async Task<bool> GenerateThumbnail(string imagePath, string savePath, int size = 720)
|
||||
public async Task<bool> GenerateThumbnail(string filePath, string savePath, int size = 720)
|
||||
{
|
||||
if (File.Exists(imagePath))
|
||||
if (_fileHelper.FileExists(filePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var filename = Path.GetFileName(imagePath);
|
||||
using (var img = await Image.LoadAsync(imagePath))
|
||||
var mediaType = GetMediaType(filePath);
|
||||
if (mediaType == MediaType.Image || mediaType == MediaType.Video)
|
||||
{
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
var filename = Path.GetFileName(filePath);
|
||||
|
||||
var orientation = this.GetOrientation(img);
|
||||
if (orientation == ImageOrientation.Square)
|
||||
if (mediaType == MediaType.Video)
|
||||
{
|
||||
width = size;
|
||||
height = size;
|
||||
}
|
||||
else if (orientation == ImageOrientation.Landscape)
|
||||
{
|
||||
var scale = (decimal)size / (decimal)img.Width;
|
||||
width = (int)((decimal)img.Width * scale);
|
||||
height = (int)((decimal)img.Height * scale);
|
||||
}
|
||||
else if (orientation == ImageOrientation.Portrait)
|
||||
{
|
||||
var scale = (decimal)size / (decimal)img.Height;
|
||||
width = (int)((decimal)img.Width * scale);
|
||||
height = (int)((decimal)img.Height * scale);
|
||||
if (FfmpegInstalled == false)
|
||||
{
|
||||
_logger.LogWarning(_localizer["FFMPEG_Downloading"].Value);
|
||||
return false;
|
||||
}
|
||||
|
||||
var conversion = await FFmpeg.Conversions.FromSnippet.Snapshot(filePath, savePath, TimeSpan.FromSeconds(0));
|
||||
await conversion.Start();
|
||||
filePath = savePath;
|
||||
}
|
||||
|
||||
img.Mutate(x =>
|
||||
using (var img = await Image.LoadAsync(filePath))
|
||||
{
|
||||
x.Resize(width, height);
|
||||
x.AutoOrient();
|
||||
});
|
||||
var width = 0;
|
||||
var height = 0;
|
||||
|
||||
await img.SaveAsWebpAsync(savePath);
|
||||
var orientation = this.GetOrientation(img);
|
||||
if (orientation == ImageOrientation.Square)
|
||||
{
|
||||
width = size;
|
||||
height = size;
|
||||
}
|
||||
else if (orientation == ImageOrientation.Landscape)
|
||||
{
|
||||
var scale = (decimal)size / (decimal)img.Width;
|
||||
width = (int)((decimal)img.Width * scale);
|
||||
height = (int)((decimal)img.Height * scale);
|
||||
}
|
||||
else if (orientation == ImageOrientation.Portrait)
|
||||
{
|
||||
var scale = (decimal)size / (decimal)img.Height;
|
||||
width = (int)((decimal)img.Width * scale);
|
||||
height = (int)((decimal)img.Height * scale);
|
||||
}
|
||||
|
||||
img.Mutate(x =>
|
||||
{
|
||||
x.Resize(width, height);
|
||||
x.AutoOrient();
|
||||
});
|
||||
|
||||
await img.SaveAsWebpAsync(savePath);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"Failed to generate thumbnail");
|
||||
_logger.LogWarning(ex, $"Failed to generate thumbnail - '{filePath}'");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public MediaType GetMediaType(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var provider = new FileExtensionContentTypeProvider();
|
||||
if (provider.TryGetContentType(path, out string? contentType))
|
||||
{
|
||||
if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MediaType.Image;
|
||||
}
|
||||
else if (contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return MediaType.Video;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return MediaType.Unknown;
|
||||
}
|
||||
|
||||
public async Task<ImageOrientation> GetOrientation(string path)
|
||||
{
|
||||
var orientation = ImageOrientation.None;
|
||||
|
||||
if (_fileHelper.FileExists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
using (var img = await Image.LoadAsync(path))
|
||||
{
|
||||
orientation = this.GetOrientation(img);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"Failed to get image orientation- '{path}'");
|
||||
}
|
||||
}
|
||||
|
||||
return orientation;
|
||||
}
|
||||
|
||||
public ImageOrientation GetOrientation(Image img)
|
||||
{
|
||||
if (img != null)
|
||||
@@ -90,5 +165,26 @@ namespace WeddingShare.Helpers
|
||||
|
||||
return ImageOrientation.None;
|
||||
}
|
||||
|
||||
public async Task<bool> DownloadFFMPEG(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!_fileHelper.DirectoryExists(path))
|
||||
{
|
||||
_fileHelper.CreateDirectoryIfNotExists(path);
|
||||
await FFmpegDownloader.GetLatestVersion(FFmpegVersion.Official, path);
|
||||
}
|
||||
|
||||
FFmpeg.SetExecutablesPath(path);
|
||||
FfmpegInstalled = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
50
WeddingShare/Helpers/LanguageHelper.cs
Normal file
50
WeddingShare/Helpers/LanguageHelper.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace WeddingShare.Helpers
|
||||
{
|
||||
public interface ILanguageHelper
|
||||
{
|
||||
public List<CultureInfo> DetectSupportedCultures();
|
||||
public Task<List<CultureInfo>> DetectSupportedCulturesAsync();
|
||||
}
|
||||
|
||||
public class LanguageHelper : ILanguageHelper
|
||||
{
|
||||
public List<CultureInfo> DetectSupportedCultures()
|
||||
{
|
||||
var supportedCultures = new List<CultureInfo>();
|
||||
|
||||
try
|
||||
{
|
||||
var resourceFiles = Directory.GetFiles(Path.Combine("Resources", "Lang"), "*.resx");
|
||||
var detectedCultures = resourceFiles
|
||||
.Select(x => Path.GetFileNameWithoutExtension(x))
|
||||
.Where(x => x.Contains("."))
|
||||
.Select(x => x.Split('.').LastOrDefault());
|
||||
|
||||
foreach (var detectedCulture in detectedCultures)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(detectedCulture))
|
||||
{
|
||||
try
|
||||
{
|
||||
supportedCultures.Add(new CultureInfo(detectedCulture));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
supportedCultures.Add(new CultureInfo("en-GB"));
|
||||
}
|
||||
|
||||
return supportedCultures;
|
||||
}
|
||||
|
||||
public Task<List<CultureInfo>> DetectSupportedCulturesAsync()
|
||||
{
|
||||
return Task.Run(DetectSupportedCultures);
|
||||
}
|
||||
}
|
||||
}
|
||||
102
WeddingShare/Helpers/Notifications/EmailHelper.cs
Normal file
102
WeddingShare/Helpers/Notifications/EmailHelper.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Net;
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace WeddingShare.Helpers.Notifications
|
||||
{
|
||||
public class EmailHelper : INotificationHelper
|
||||
{
|
||||
private readonly ISettingsHelper _settings;
|
||||
private readonly ISmtpClientWrapper _client;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public EmailHelper(ISettingsHelper settings, ISmtpClientWrapper client, ILogger<EmailHelper> logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_client = client;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Send(string title, string message, string? actionLink = null)
|
||||
{
|
||||
if (await _settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, false))
|
||||
{
|
||||
try
|
||||
{
|
||||
var recipients = (await _settings.GetOrDefault(Constants.Notifications.Smtp.Recipient, string.Empty))?.Split(new char[] { ';', ',' }, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)?.Select(x => new MailAddress(x));
|
||||
if (recipients != null && recipients.Any())
|
||||
{
|
||||
var host = await _settings.GetOrDefault(Constants.Notifications.Smtp.Host, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
var port = await _settings.GetOrDefault(Constants.Notifications.Smtp.Port, 587);
|
||||
if (port > 0)
|
||||
{
|
||||
var from = await _settings.GetOrDefault(Constants.Notifications.Smtp.From, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(from))
|
||||
{
|
||||
var sentToAll = true;
|
||||
using (var smtp = new SmtpClient(host, port))
|
||||
{
|
||||
var username = await _settings.GetOrDefault(Constants.Notifications.Smtp.Username, string.Empty);
|
||||
var password = await _settings.GetOrDefault(Constants.Notifications.Smtp.Password, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(username) && !string.IsNullOrWhiteSpace(password))
|
||||
{
|
||||
smtp.UseDefaultCredentials = false;
|
||||
smtp.Credentials = new NetworkCredential(username, password);
|
||||
}
|
||||
|
||||
smtp.EnableSsl = await _settings.GetOrDefault(Constants.Notifications.Smtp.UseSSL, false);
|
||||
|
||||
var sender = new MailAddress(from, await _settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, "WeddingShare"));
|
||||
foreach (var to in recipients)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _client.SendMailAsync(smtp, new MailMessage(new MailAddress(from, await _settings.GetOrDefault(Constants.Notifications.Smtp.DisplayName, "WeddingShare")), to)
|
||||
{
|
||||
Sender = sender,
|
||||
Subject = title,
|
||||
Body = !string.IsNullOrWhiteSpace(actionLink) ? $"{message}<br/><br/>Visit - {actionLink}" : message,
|
||||
IsBodyHtml = true,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"Failed to send email to '{to}' - {ex.Message}");
|
||||
sentToAll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sentToAll;
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Invalid SMTP sender specified");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Invalid SMTP port specified");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Invalid SMTP host specified");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Invalid SMTP recipient specified");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Failed to send email with title '{title}' - {ex?.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
75
WeddingShare/Helpers/Notifications/GotifyHelper.cs
Normal file
75
WeddingShare/Helpers/Notifications/GotifyHelper.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
namespace WeddingShare.Helpers.Notifications
|
||||
{
|
||||
public class GotifyHelper : INotificationHelper
|
||||
{
|
||||
private readonly ISettingsHelper _settings;
|
||||
private readonly IHttpClientFactory _clientFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public GotifyHelper(ISettingsHelper settings, IHttpClientFactory clientFactory, ILogger<GotifyHelper> logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_clientFactory = clientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Send(string title, string message, string? actionLink = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(await _settings.GetOrDefault(Constants.Notifications.Gotify.Endpoint, string.Empty)))
|
||||
{
|
||||
_logger.LogWarning($"Invalid Gotify endpoint specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(await _settings.GetOrDefault(Constants.Notifications.Gotify.Token, string.Empty)))
|
||||
{
|
||||
_logger.LogWarning($"Invalid Gotify token specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await _settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, false))
|
||||
{
|
||||
try
|
||||
{
|
||||
var token = await _settings.GetOrDefault(Constants.Notifications.Gotify.Token, string.Empty);
|
||||
if (!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
var priority = await _settings.GetOrDefault(Constants.Notifications.Gotify.Priority, 4);
|
||||
if (priority > 0)
|
||||
{
|
||||
message = !string.IsNullOrWhiteSpace(actionLink) ? $"{message} - Visit - {actionLink}" : message;
|
||||
|
||||
var client = _clientFactory.CreateClient("GotifyClient");
|
||||
using (var response = await client.PostAsJsonAsync($"/message?token={token}", new { title, message, priority }))
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError($"Failed to send Gotify message with title '{title}' - {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Invalid Gotify priority specified");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Invalid Gotify token specified");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Failed to send Gotify message with title '{title}' - {ex?.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace WeddingShare.Helpers.Notifications
|
||||
{
|
||||
public interface INotificationHelper
|
||||
{
|
||||
Task<bool> Send(string title, string message, string? actionLink = null);
|
||||
}
|
||||
}
|
||||
42
WeddingShare/Helpers/Notifications/NotificationBroker.cs
Normal file
42
WeddingShare/Helpers/Notifications/NotificationBroker.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace WeddingShare.Helpers.Notifications
|
||||
{
|
||||
public class NotificationBroker : INotificationHelper
|
||||
{
|
||||
private readonly ISettingsHelper _settings;
|
||||
private readonly ISmtpClientWrapper _smtp;
|
||||
private readonly IHttpClientFactory _clientFactory;
|
||||
private readonly ILoggerFactory _logger;
|
||||
|
||||
public NotificationBroker(ISettingsHelper settings, ISmtpClientWrapper smtp, IHttpClientFactory clientFactory, ILoggerFactory logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_smtp = smtp;
|
||||
_clientFactory = clientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Send(string title, string message, string? actionLink = null)
|
||||
{
|
||||
var emailSent = true;
|
||||
var ntfySent = true;
|
||||
var gotifySent = true;
|
||||
|
||||
if (await _settings.GetOrDefault(Constants.Notifications.Smtp.Enabled, false))
|
||||
{
|
||||
emailSent = await new EmailHelper(_settings, _smtp, _logger.CreateLogger<EmailHelper>()).Send(title, message, actionLink);
|
||||
}
|
||||
|
||||
if (await _settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, false))
|
||||
{
|
||||
ntfySent = await new NtfyHelper(_settings, _clientFactory, _logger.CreateLogger<NtfyHelper>()).Send(title, message, actionLink);
|
||||
}
|
||||
|
||||
if (await _settings.GetOrDefault(Constants.Notifications.Gotify.Enabled, false))
|
||||
{
|
||||
gotifySent = await new GotifyHelper(_settings, _clientFactory, _logger.CreateLogger<GotifyHelper>()).Send(title, message, actionLink);
|
||||
}
|
||||
|
||||
return emailSent && ntfySent && gotifySent;
|
||||
}
|
||||
}
|
||||
}
|
||||
79
WeddingShare/Helpers/Notifications/NtfyHelper.cs
Normal file
79
WeddingShare/Helpers/Notifications/NtfyHelper.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using WeddingShare.Constants;
|
||||
|
||||
namespace WeddingShare.Helpers.Notifications
|
||||
{
|
||||
public class NtfyHelper : INotificationHelper
|
||||
{
|
||||
private readonly ISettingsHelper _settings;
|
||||
private readonly IHttpClientFactory _clientFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public NtfyHelper(ISettingsHelper settings, IHttpClientFactory clientFactory, ILogger<NtfyHelper> logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_clientFactory = clientFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<bool> Send(string title, string message, string? actionLink = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(await _settings.GetOrDefault(Constants.Notifications.Ntfy.Endpoint, string.Empty)))
|
||||
{
|
||||
_logger.LogWarning($"Invalid Ntfy endpoint specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(await _settings.GetOrDefault(Constants.Notifications.Ntfy.Token, string.Empty)))
|
||||
{
|
||||
_logger.LogWarning($"Invalid Ntfy token specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await _settings.GetOrDefault(Constants.Notifications.Ntfy.Enabled, false))
|
||||
{
|
||||
try
|
||||
{
|
||||
var topic = await _settings.GetOrDefault(Constants.Notifications.Ntfy.Topic, "WeddingShare");
|
||||
if (!string.IsNullOrWhiteSpace(topic))
|
||||
{
|
||||
var priority = await _settings.GetOrDefault(Constants.Notifications.Ntfy.Priority, 4);
|
||||
if (priority > 0)
|
||||
{
|
||||
var defaultIcon = "https://github.com/Cirx08/WeddingShare/blob/main/WeddingShare/wwwroot/images/logo.png?raw=true";
|
||||
var icon = await _settings.GetOrDefault(Settings.Basic.Logo, defaultIcon);
|
||||
icon = !icon.StartsWith('.') && !icon.StartsWith('/') ? icon : defaultIcon;
|
||||
|
||||
var client = _clientFactory.CreateClient("NtfyClient");
|
||||
using (var response = await client.PostAsJsonAsync("/", new { icon, topic, title, message, priority, click = actionLink }))
|
||||
{
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError($"Failed to send Ntfy message with title '{title}' - {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Invalid Ntfy priority specified");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning($"Invalid Ntfy topic specified");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Failed to send Ntfy message with title '{title}' - {ex?.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using WeddingShare.Helpers.Database;
|
||||
|
||||
namespace WeddingShare.Helpers
|
||||
{
|
||||
public interface ISecretKeyHelper
|
||||
{
|
||||
Task<string?> GetGallerySecretKey(string galleryId);
|
||||
}
|
||||
|
||||
public class SecretKeyHelper : ISecretKeyHelper
|
||||
{
|
||||
private readonly IConfigHelper _config;
|
||||
private readonly IDatabaseHelper _database;
|
||||
|
||||
public SecretKeyHelper(IConfigHelper config, IDatabaseHelper database)
|
||||
{
|
||||
_config = config;
|
||||
_database = database;
|
||||
}
|
||||
|
||||
public async Task<string?> GetGallerySecretKey(string galleryId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var secretKey = _config.Get("Settings", $"Secret_Key_{galleryId}");
|
||||
if (string.IsNullOrWhiteSpace(secretKey))
|
||||
{
|
||||
secretKey = (await _database.GetGallery(galleryId))?.SecretKey;
|
||||
if (string.IsNullOrWhiteSpace(secretKey))
|
||||
{
|
||||
secretKey = _config.Get("Settings", "Secret_Key");
|
||||
}
|
||||
}
|
||||
|
||||
return secretKey;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
201
WeddingShare/Helpers/SettingsHelper.cs
Normal file
201
WeddingShare/Helpers/SettingsHelper.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
using WeddingShare.Helpers.Database;
|
||||
using WeddingShare.Models.Database;
|
||||
|
||||
namespace WeddingShare.Helpers
|
||||
{
|
||||
public interface ISettingsHelper
|
||||
{
|
||||
Task<SettingModel?> Get(string key, string? gallery = "");
|
||||
Task<string> GetOrDefault(string key, string defaultValue, string? gallery = "");
|
||||
Task<int> GetOrDefault(string key, int defaultValue, string? gallery = "");
|
||||
Task<long> GetOrDefault(string key, long defaultValue, string? gallery = "");
|
||||
Task<decimal> GetOrDefault(string key, decimal defaultValue, string? gallery = "");
|
||||
Task<double> GetOrDefault(string key, double defaultValue, string? gallery = "");
|
||||
Task<bool> GetOrDefault(string key, bool defaultValue, string? gallery = "");
|
||||
Task<DateTime?> GetOrDefault(string key, DateTime? defaultValue, string? gallery = "");
|
||||
Task<SettingModel?> SetSetting(string key, string value, string? gallery = "");
|
||||
Task<bool> DeleteSetting(string key, string? gallery = "");
|
||||
}
|
||||
|
||||
public class SettingsHelper : ISettingsHelper
|
||||
{
|
||||
private readonly IDatabaseHelper _databaseHelper;
|
||||
private readonly IConfigHelper _configHelper;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SettingsHelper(IDatabaseHelper databaseHelper, IConfigHelper configHelper, ILogger<SettingsHelper> logger)
|
||||
{
|
||||
_databaseHelper = databaseHelper;
|
||||
_configHelper = configHelper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<SettingModel?> Get(string key, string? gallery = "")
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
try
|
||||
{
|
||||
var dbValue = await _databaseHelper.GetSetting(key, gallery);
|
||||
if (dbValue != null)
|
||||
{
|
||||
return dbValue;
|
||||
}
|
||||
|
||||
var configValue = _configHelper.Get(key);
|
||||
if (configValue != null)
|
||||
{
|
||||
return new SettingModel()
|
||||
{
|
||||
Id = key.ToUpper(),
|
||||
Value = configValue
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, $"Failed to find key '{key}' in either database or config");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<string> GetOrDefault(string key, string defaultValue, string? gallery = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = (await this.Get(key, gallery))?.Value;
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public async Task<int> GetOrDefault(string key, int defaultValue, string? gallery = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = await this.GetOrDefault(key, string.Empty, gallery);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToInt32(value);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public async Task<long> GetOrDefault(string key, long defaultValue, string? gallery = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = await this.GetOrDefault(key, string.Empty, gallery);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToInt64(value);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public async Task<decimal> GetOrDefault(string key, decimal defaultValue, string? gallery = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = await this.GetOrDefault(key, string.Empty, gallery);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToDecimal(value);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public async Task<double> GetOrDefault(string key, double defaultValue, string? gallery = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = await this.GetOrDefault(key, string.Empty, gallery);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToDouble(value);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public async Task<bool> GetOrDefault(string key, bool defaultValue, string? gallery = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = await this.GetOrDefault(key, string.Empty, gallery);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToBoolean(value);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public async Task<DateTime?> GetOrDefault(string key, DateTime? defaultValue, string? gallery = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = await this.GetOrDefault(key, string.Empty, gallery);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Convert.ToDateTime(value);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public async Task<SettingModel?> SetSetting(string key, string value, string? gallery = "")
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return await _databaseHelper.SetSetting(new SettingModel()
|
||||
{
|
||||
Id = key,
|
||||
Value = value
|
||||
}, gallery);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteSetting(string key, string? gallery = "")
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
return await _databaseHelper.DeleteSetting(new SettingModel()
|
||||
{
|
||||
Id = key.ToUpper()
|
||||
}, gallery);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAllSettings(string? gallery = "")
|
||||
{
|
||||
return await _databaseHelper.DeleteAllSettings(gallery);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
WeddingShare/Helpers/SmtpClientWrapper.cs
Normal file
17
WeddingShare/Helpers/SmtpClientWrapper.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Net.Mail;
|
||||
|
||||
namespace WeddingShare.Helpers
|
||||
{
|
||||
public interface ISmtpClientWrapper
|
||||
{
|
||||
Task SendMailAsync(SmtpClient client, MailMessage message);
|
||||
}
|
||||
|
||||
public class SmtpClientWrapper : ISmtpClientWrapper
|
||||
{
|
||||
public async Task SendMailAsync(SmtpClient client, MailMessage message)
|
||||
{
|
||||
await client.SendMailAsync(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
97
WeddingShare/Helpers/UrlHelper.cs
Normal file
97
WeddingShare/Helpers/UrlHelper.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Web;
|
||||
using WeddingShare.Constants;
|
||||
|
||||
namespace WeddingShare.Helpers
|
||||
{
|
||||
public interface IUrlHelper
|
||||
{
|
||||
public string GenerateFullUrl(HttpRequest? ctx, string? path, List<KeyValuePair<string, string>>? append = null, List<string>? exclude = null);
|
||||
public string GenerateBaseUrl(HttpRequest? ctx, string? path);
|
||||
public string GenerateQueryString(HttpRequest? ctx, List<KeyValuePair<string, string>>? append = null, List<string>? exclude = null);
|
||||
public string ExtractHost(string value);
|
||||
public string ExtractQueryValue(HttpRequest? ctx, string key);
|
||||
}
|
||||
|
||||
public class UrlHelper : IUrlHelper
|
||||
{
|
||||
private readonly ISettingsHelper _settings;
|
||||
|
||||
public UrlHelper(ISettingsHelper settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
public string GenerateFullUrl(HttpRequest? ctx, string? path, List<KeyValuePair<string, string>>? append = null, List<string>? exclude = null)
|
||||
{
|
||||
return $"{GenerateBaseUrl(ctx, path)}{GenerateQueryString(ctx, append, exclude)}";
|
||||
}
|
||||
|
||||
public string GenerateBaseUrl(HttpRequest? ctx, string? path)
|
||||
{
|
||||
if (ctx != null)
|
||||
{
|
||||
var scheme = _settings.GetOrDefault(Settings.Basic.ForceHttps, false).Result ? "https" : ctx.Scheme;
|
||||
var host = ExtractHost(_settings.GetOrDefault(Settings.Basic.BaseUrl, ctx.Host.Value).Result);
|
||||
|
||||
return $"{scheme}://{host}/{path?.TrimStart('/')}";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string GenerateQueryString(HttpRequest? ctx, List<KeyValuePair<string, string>>? append = null, List<string>? exclude = null)
|
||||
{
|
||||
if (ctx != null)
|
||||
{
|
||||
append = append ?? new List<KeyValuePair<string, string>>();
|
||||
exclude = exclude ?? new List<string>();
|
||||
|
||||
foreach (var a in append)
|
||||
{
|
||||
exclude.Add(a.Key);
|
||||
}
|
||||
|
||||
var queryString = new StringBuilder();
|
||||
foreach (var q in ctx.Query.Where(x => !exclude.Any(f => f.Equals(x.Key, StringComparison.OrdinalIgnoreCase))))
|
||||
{
|
||||
queryString.Append($"&{HttpUtility.UrlEncode(q.Key)}={HttpUtility.UrlEncode(q.Value)}");
|
||||
}
|
||||
|
||||
foreach (var a in append)
|
||||
{
|
||||
queryString.Append($"&{HttpUtility.UrlEncode(a.Key)}={HttpUtility.UrlEncode(a.Value)}");
|
||||
}
|
||||
|
||||
return $"?{queryString.ToString().Trim('&')}".TrimEnd('?');
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string ExtractHost(string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return Regex.Replace(value, "http[s]*\\:\\/\\/", string.Empty).Trim('/');
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public string ExtractQueryValue(HttpRequest? ctx, string key)
|
||||
{
|
||||
if (ctx?.Query != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ctx.Query.FirstOrDefault(x => string.Equals(key, x.Key, StringComparison.OrdinalIgnoreCase)).Value.ToString();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
6
WeddingShare/Lang/Translations.cs
Normal file
6
WeddingShare/Lang/Translations.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace WeddingShare.Lang
|
||||
{
|
||||
public class Translations
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,36 @@ namespace WeddingShare.Models.Database
|
||||
{
|
||||
public class GalleryItemModel
|
||||
{
|
||||
public GalleryItemModel()
|
||||
: this(0, 0, string.Empty, string.Empty, null, null, null, MediaType.Unknown, ImageOrientation.None, GalleryItemState.Pending, 0)
|
||||
{
|
||||
}
|
||||
|
||||
public GalleryItemModel(int id, int galleryId, string galleryName, string title, string? uploadedBy, DateTime? uploadedDate, string? checksum, MediaType mediaType, ImageOrientation orientation, GalleryItemState state, long file_size)
|
||||
{
|
||||
Id = id;
|
||||
GalleryId = galleryId;
|
||||
GalleryName = galleryName;
|
||||
Title = title;
|
||||
UploadedBy = uploadedBy;
|
||||
UploadedDate = uploadedDate;
|
||||
Checksum = checksum;
|
||||
MediaType = mediaType;
|
||||
Orientation = orientation;
|
||||
State = state;
|
||||
FileSize = file_size;
|
||||
}
|
||||
|
||||
public int Id { get; set; }
|
||||
public int GalleryId { get; set; }
|
||||
public string GalleryName { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string? UploadedBy { get; set; }
|
||||
public DateTime? UploadedDate { get; set; }
|
||||
public string? Checksum { get; set; }
|
||||
public MediaType MediaType { get; set; }
|
||||
public ImageOrientation Orientation { get; set; }
|
||||
public GalleryItemState State { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,11 @@
|
||||
public int TotalItems { get; set; }
|
||||
public int ApprovedItems { get; set; }
|
||||
public int PendingItems { get; set; }
|
||||
public long TotalGallerySize { get; set; }
|
||||
|
||||
public string CalculateUsage(long maxSizeMB = long.MaxValue)
|
||||
{
|
||||
return ((double)(TotalGallerySize / (double)(maxSizeMB * 1000000L))).ToString("0.00%");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace WeddingShare.Models.Database
|
||||
{
|
||||
public class PendingGalleryItemModel : GalleryItemModel
|
||||
{
|
||||
public string GalleryName { get; set; }
|
||||
}
|
||||
}
|
||||
22
WeddingShare/Models/Database/SettingModel.cs
Normal file
22
WeddingShare/Models/Database/SettingModel.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace WeddingShare.Models.Database
|
||||
{
|
||||
public class SettingModel
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string? Value { get; set; }
|
||||
|
||||
public T Parse<T>(T defaultValue)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(this.Value))
|
||||
{
|
||||
try
|
||||
{
|
||||
return (T)Convert.ChangeType(this.Value, typeof(T));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,20 @@
|
||||
public class UserModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string? Email { get; set; }
|
||||
public string? Password { get; set; }
|
||||
public string? CPassword { get; set; }
|
||||
public int FailedLogins { get; set; }
|
||||
public DateTime? LockoutUntil { get; set; }
|
||||
public string? MultiFactorToken { get; set; }
|
||||
|
||||
public bool IsLockedOut
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.LockoutUntil != null && this.LockoutUntil >= DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
WeddingShare/Models/ExportOptions.cs
Normal file
12
WeddingShare/Models/ExportOptions.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace WeddingShare.Models
|
||||
{
|
||||
public class ExportOptions
|
||||
{
|
||||
public bool Database { get; set; } = true;
|
||||
public bool Uploads { get; set; } = true;
|
||||
public bool Thumbnails { get; set; } = true;
|
||||
public bool Logos { get; set; } = true;
|
||||
public bool Banners { get; set; } = true;
|
||||
public bool CustomResources { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,17 @@
|
||||
{
|
||||
public class FileUploader
|
||||
{
|
||||
public FileUploader(string id, string url)
|
||||
public FileUploader(string id, string? key, string url, bool identityRequired = false)
|
||||
{
|
||||
this.GalleryId = id;
|
||||
this.SecretKey = key;
|
||||
this.UploadUrl = url;
|
||||
this.IdentityRequired = identityRequired;
|
||||
}
|
||||
|
||||
public string? GalleryId { get; set; }
|
||||
public string? SecretKey { get; set; }
|
||||
public string? UploadUrl { get; set; }
|
||||
public bool IdentityRequired { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,13 @@
|
||||
{
|
||||
public LoginModel()
|
||||
{
|
||||
this.Username = string.Empty;
|
||||
this.Password = string.Empty;
|
||||
this.Code = string.Empty;
|
||||
}
|
||||
|
||||
public string? Password { get; set; }
|
||||
public string Username { get; set; }
|
||||
public string Password { get; set; }
|
||||
public string Code { get; set; }
|
||||
}
|
||||
}
|
||||
16
WeddingShare/Models/Migrator/KeyMigrator.cs
Normal file
16
WeddingShare/Models/Migrator/KeyMigrator.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace WeddingShare.Models.Migrator
|
||||
{
|
||||
public class KeyMigrator
|
||||
{
|
||||
public KeyMigrator(int priority, string key, Func<string, string>? action = null)
|
||||
{
|
||||
Priority = priority;
|
||||
Key = key;
|
||||
MigrationAction = action;
|
||||
}
|
||||
|
||||
public string Key { get; set; }
|
||||
public Func<string, string>? MigrationAction { get; set; }
|
||||
public int Priority { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -5,40 +5,38 @@ namespace WeddingShare.Models
|
||||
public class PhotoGallery
|
||||
{
|
||||
public PhotoGallery()
|
||||
: this(3, ViewMode.Default)
|
||||
: this(ViewMode.Default, GalleryGroup.None, GalleryOrder.Descending)
|
||||
{
|
||||
}
|
||||
|
||||
public PhotoGallery(int columnCount, ViewMode viewMode)
|
||||
: this("default", columnCount, string.Empty, string.Empty, viewMode, new List<PhotoGalleryImage>())
|
||||
public PhotoGallery(ViewMode viewMode, GalleryGroup groupBy, GalleryOrder orderBy)
|
||||
: this(1, "default", string.Empty, viewMode, groupBy, orderBy, new List<PhotoGalleryImage>(), false)
|
||||
{
|
||||
}
|
||||
|
||||
public PhotoGallery(string id, int columnCount, string galleryPath, string thumbnailPath, ViewMode viewMode, List<PhotoGalleryImage> images)
|
||||
public PhotoGallery(int id, string name, string secretKey, ViewMode viewMode, GalleryGroup groupBy, GalleryOrder orderBy, List<PhotoGalleryImage> images, bool requireIdentity)
|
||||
{
|
||||
this.GalleryId = id;
|
||||
this.GalleryPath = galleryPath;
|
||||
this.ThumbnailsPath = thumbnailPath;
|
||||
this.GalleryName = name;
|
||||
this.ViewMode = viewMode;
|
||||
this.ColumnCount = columnCount;
|
||||
this.GroupBy = groupBy;
|
||||
this.OrderBy = orderBy;
|
||||
this.PendingCount = 0;
|
||||
this.Images = images;
|
||||
this.FileUploader = new FileUploader(id, "/Gallery/UploadImage");
|
||||
this.FileUploader = new FileUploader(name, secretKey, "/Gallery/UploadImage", requireIdentity);
|
||||
}
|
||||
|
||||
public string? GalleryId { get; set; }
|
||||
public string? GalleryPath { get; set; }
|
||||
public string? ThumbnailsPath { get; set; }
|
||||
public int? GalleryId { get; set; }
|
||||
public string? GalleryName { get; set; }
|
||||
public ViewMode ViewMode { get; set; }
|
||||
public int ColumnCount { get; set; }
|
||||
public GalleryGroup GroupBy { get; set; }
|
||||
public GalleryOrder OrderBy { get; set; }
|
||||
public int ApprovedCount { get; set; }
|
||||
public int PendingCount { get; set; }
|
||||
public int ApprovedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Images?.Count ?? 0;
|
||||
}
|
||||
}
|
||||
public int ItemsPerPage { get; set; } = 50;
|
||||
public int CurrentPage { get; set; } = 1;
|
||||
public bool Pagination { get; set; } = true;
|
||||
public bool LoadScripts { get; set; } = true;
|
||||
public int TotalCount
|
||||
{
|
||||
get
|
||||
@@ -57,7 +55,13 @@ namespace WeddingShare.Models
|
||||
}
|
||||
|
||||
public int Id { get; set; }
|
||||
public string? Path { get; set; }
|
||||
public int? GalleryId { get; set; }
|
||||
public string? GalleryName { get; set; }
|
||||
public string? Name { get; set; }
|
||||
public string? UploadedBy { get; set; }
|
||||
public DateTime? UploadDate { get; set; }
|
||||
public string? ImagePath { get; set; }
|
||||
public string? ThumbnailPath { get; set; }
|
||||
public MediaType MediaType { get; set; }
|
||||
}
|
||||
}
|
||||
12
WeddingShare/Models/SessionKey.cs
Normal file
12
WeddingShare/Models/SessionKey.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace WeddingShare.Models
|
||||
{
|
||||
public static class SessionKey
|
||||
{
|
||||
public const string DeviceType = "DeviceType";
|
||||
public const string ViewerIdentity = "ViewerIdentity";
|
||||
public const string MultiFactorTokenSet = "2FA_SET";
|
||||
public const string MultiFactorSecret = "2FA_SECRET";
|
||||
public const string MultiFactorQR = "2FA_QR_CODE";
|
||||
public const string SelectedLanguage = "SelectedLanguage";
|
||||
}
|
||||
}
|
||||
9
WeddingShare/Models/SupportedLanguage.cs
Normal file
9
WeddingShare/Models/SupportedLanguage.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace WeddingShare.Models
|
||||
{
|
||||
public class SupportedLanguage
|
||||
{
|
||||
public string Key { get; set; }
|
||||
public string Value { get; set; }
|
||||
public bool Selected { get; set; }
|
||||
}
|
||||
}
|
||||
13
WeddingShare/Models/UpdateSettingsModel.cs
Normal file
13
WeddingShare/Models/UpdateSettingsModel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Runtime.Serialization;
|
||||
|
||||
namespace WeddingShare.Models
|
||||
{
|
||||
public class UpdateSettingsModel
|
||||
{
|
||||
[DataMember(Name = "key")]
|
||||
public string Key { get; set; }
|
||||
|
||||
[DataMember(Name = "value")]
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,6 @@ namespace WeddingShare
|
||||
.UseIIS()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseStartup<Startup>()
|
||||
.UseUrls("http://*:5000/");
|
||||
.UseUrls("http://*:5000");
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,15 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5051"
|
||||
"applicationUrl": "http://localhost:5000"
|
||||
},
|
||||
"Docker": {
|
||||
"commandName": "Docker",
|
||||
"launchBrowser": true,
|
||||
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_HTTPS_PORTS": "8081",
|
||||
"ASPNETCORE_HTTP_PORTS": "8080"
|
||||
"ASPNETCORE_HTTPS_PORTS": "5001",
|
||||
"ASPNETCORE_HTTP_PORTS": "5000"
|
||||
},
|
||||
"publishAllPorts": true,
|
||||
"useSSL": true
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Failed_Add_Gallery" xml:space="preserve">
|
||||
<value>Failed to create gallery</value>
|
||||
</data>
|
||||
<data name="Failed_Delete_Gallery" xml:space="preserve">
|
||||
<value>Failed to delete gallery</value>
|
||||
</data>
|
||||
<data name="Failed_Download_Gallery" xml:space="preserve">
|
||||
<value>Failed to download gallery</value>
|
||||
</data>
|
||||
<data name="Failed_Edit_Gallery" xml:space="preserve">
|
||||
<value>Failed to update gallery</value>
|
||||
</data>
|
||||
<data name="Failed_Export" xml:space="preserve">
|
||||
<value>Failed to export data</value>
|
||||
</data>
|
||||
<data name="Failed_Finding_File" xml:space="preserve">
|
||||
<value>Failed to find file</value>
|
||||
</data>
|
||||
<data name="Failed_Import" xml:space="preserve">
|
||||
<value>Failed to import data</value>
|
||||
</data>
|
||||
<data name="Failed_Reviewing_Photo" xml:space="preserve">
|
||||
<value>Failed to review photo</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Galleries" xml:space="preserve">
|
||||
<value>Failed to wipe galleries</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Gallery" xml:space="preserve">
|
||||
<value>Failed to wipe gallery</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Already_Exists" xml:space="preserve">
|
||||
<value>Gallery name already exists</value>
|
||||
</data>
|
||||
<data name="Login_Failed" xml:space="preserve">
|
||||
<value>Faied to log user in</value>
|
||||
</data>
|
||||
<data name="Name_Cannot_Be_Blank" xml:space="preserve">
|
||||
<value>Gallery name cannot be empty</value>
|
||||
</data>
|
||||
<data name="Pending_Uploads_Failed" xml:space="preserve">
|
||||
<value>Failed to get pending uploads</value>
|
||||
</data>
|
||||
<data name="Unknown_Review_Action" xml:space="preserve">
|
||||
<value>Unknown review action</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,165 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="Failed_Add_Gallery" xml:space="preserve">
|
||||
<value>Impossible de créer la galerie</value>
|
||||
</data>
|
||||
<data name="Failed_Delete_Gallery" xml:space="preserve">
|
||||
<value>Impossible de supprimer la galerie</value>
|
||||
</data>
|
||||
<data name="Failed_Download_Gallery" xml:space="preserve">
|
||||
<value>Impossible de télécharger la galerie</value>
|
||||
</data>
|
||||
<data name="Failed_Edit_Gallery" xml:space="preserve">
|
||||
<value>Échec de la mise à jour de la galerie</value>
|
||||
</data>
|
||||
<data name="Failed_Export" xml:space="preserve">
|
||||
<value>Échec de l'exportation des données</value>
|
||||
</data>
|
||||
<data name="Failed_Finding_File" xml:space="preserve">
|
||||
<value>Impossible de trouver le fichier</value>
|
||||
</data>
|
||||
<data name="Failed_Import" xml:space="preserve">
|
||||
<value>Échec de l'importation des données</value>
|
||||
</data>
|
||||
<data name="Failed_Reviewing_Photo" xml:space="preserve">
|
||||
<value>Impossible de vérifier la photo</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Galleries" xml:space="preserve">
|
||||
<value>Impossible d'effacer la galerie</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Gallery" xml:space="preserve">
|
||||
<value>Impossible d'effacer la galerie</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Already_Exists" xml:space="preserve">
|
||||
<value>Le nom de la galerie existe déjà</value>
|
||||
</data>
|
||||
<data name="Login_Failed" xml:space="preserve">
|
||||
<value>Impossible de connecter l'utilisateur</value>
|
||||
</data>
|
||||
<data name="Name_Cannot_Be_Blank" xml:space="preserve">
|
||||
<value>Le nom de la galerie ne peut pas être vide</value>
|
||||
</data>
|
||||
<data name="Pending_Uploads_Failed" xml:space="preserve">
|
||||
<value>Impossible d'obtenir les téléchargements en attente</value>
|
||||
</data>
|
||||
<data name="Unknown_Review_Action" xml:space="preserve">
|
||||
<value>Action de révision inconnue</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,153 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="File_Upload_Failed" xml:space="preserve">
|
||||
<value>Failed to upload file</value>
|
||||
</data>
|
||||
<data name="Gallery_Does_Not_Exist" xml:space="preserve">
|
||||
<value>The requested gallery was not found or does not exist</value>
|
||||
</data>
|
||||
<data name="Image_Upload_Failed" xml:space="preserve">
|
||||
<value>Failed to upload images</value>
|
||||
</data>
|
||||
<data name="Invalid_Access_Token" xml:space="preserve">
|
||||
<value>The provided access token was invalid</value>
|
||||
</data>
|
||||
<data name="Invalid_File_Type" xml:space="preserve">
|
||||
<value>File type is invalid</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Id" xml:space="preserve">
|
||||
<value>Invalid gallery Id</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Key" xml:space="preserve">
|
||||
<value>Invalid gallery key</value>
|
||||
</data>
|
||||
<data name="Invalid_Secret_Key_Warning" xml:space="preserve">
|
||||
<value>A request was made using an invalid secret key</value>
|
||||
</data>
|
||||
<data name="Max_File_Size" xml:space="preserve">
|
||||
<value>Max file size is</value>
|
||||
</data>
|
||||
<data name="No_Files_For_Upload" xml:space="preserve">
|
||||
<value>No files were detected for upload</value>
|
||||
</data>
|
||||
<data name="Save_To_Gallery_Failed" xml:space="preserve">
|
||||
<value>Failed to save image to gallery</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,153 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="File_Upload_Failed" xml:space="preserve">
|
||||
<value>Impossible de télécharger le fichier</value>
|
||||
</data>
|
||||
<data name="Gallery_Does_Not_Exist" xml:space="preserve">
|
||||
<value>La galerie demandée n'a pas été trouvée ou n'existe pas</value>
|
||||
</data>
|
||||
<data name="Image_Upload_Failed" xml:space="preserve">
|
||||
<value>Impossible de télécharger les images</value>
|
||||
</data>
|
||||
<data name="Invalid_Access_Token" xml:space="preserve">
|
||||
<value>Le jeton d'accès fourni n'était pas valide</value>
|
||||
</data>
|
||||
<data name="Invalid_File_Type" xml:space="preserve">
|
||||
<value>Le type de fichier n'est pas valide</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Id" xml:space="preserve">
|
||||
<value>ID de galerie invalide</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Key" xml:space="preserve">
|
||||
<value>Clé de galerie non valide</value>
|
||||
</data>
|
||||
<data name="Invalid_Secret_Key_Warning" xml:space="preserve">
|
||||
<value>Une demande a été effectuée à l'aide d'une clé de sécurité non valide</value>
|
||||
</data>
|
||||
<data name="Max_File_Size" xml:space="preserve">
|
||||
<value>La taille maximale du fichier est</value>
|
||||
</data>
|
||||
<data name="No_Files_For_Upload" xml:space="preserve">
|
||||
<value>Aucun fichier n'a été détecté pour le téléchargement</value>
|
||||
</data>
|
||||
<data name="Save_To_Gallery_Failed" xml:space="preserve">
|
||||
<value>Impossible d'enregistrer l'image dans la galerie</value>
|
||||
</data>
|
||||
</root>
|
||||
901
WeddingShare/Resources/Lang/Translations.ar-001.resx
Normal file
901
WeddingShare/Resources/Lang/Translations.ar-001.resx
Normal file
@@ -0,0 +1,901 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="400_Error_Message" xml:space="preserve">
|
||||
<value>إذا استمرت هذه المسألة، يرجى الاتصال بمدير الإنترنت</value>
|
||||
</data>
|
||||
<data name="400_Error_Title" xml:space="preserve">
|
||||
<value>يبدو أن خطأ غير متوقع حدث.</value>
|
||||
</data>
|
||||
<data name="401_Error_Message" xml:space="preserve">
|
||||
<value>يبدو أنك غير مصرح لك بالنظر إلى تلك الصفحة.</value>
|
||||
</data>
|
||||
<data name="401_Error_Title" xml:space="preserve">
|
||||
<value>Access Denied</value>
|
||||
</data>
|
||||
<data name="402_Error_Message" xml:space="preserve">
|
||||
<value>المفتاح السري كان غير صحيح</value>
|
||||
</data>
|
||||
<data name="402_Error_Title" xml:space="preserve">
|
||||
<value>مفتاح سرّي غير مستقر</value>
|
||||
</data>
|
||||
<data name="403_Error_Message" xml:space="preserve">
|
||||
<value>المجوهرات المحددة لا وجود لها وليس لديك إذن لخلقها</value>
|
||||
</data>
|
||||
<data name="403_Error_Title" xml:space="preserve">
|
||||
<value>Access Denied</value>
|
||||
</data>
|
||||
<data name="405_Error_Message" xml:space="preserve">
|
||||
<value>لقد وصلت إلى الحد الأقصى لخلق المعرض</value>
|
||||
</data>
|
||||
<data name="405_Error_Title" xml:space="preserve">
|
||||
<value>Limit Reached</value>
|
||||
</data>
|
||||
<data name="406_Error_Message" xml:space="preserve">
|
||||
<value>المعرض المحدد لا يوجد</value>
|
||||
</data>
|
||||
<data name="406_Error_Title" xml:space="preserve">
|
||||
<value>جالري</value>
|
||||
</data>
|
||||
<data name="Actions" xml:space="preserve">
|
||||
<value>الإجراءات</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>مضافا إليها</value>
|
||||
</data>
|
||||
<data name="Approve" xml:space="preserve">
|
||||
<value>الموافقة</value>
|
||||
</data>
|
||||
<data name="Approved" xml:space="preserve">
|
||||
<value>الوظائف المعتمدة</value>
|
||||
</data>
|
||||
<data name="Available_Galleries" xml:space="preserve">
|
||||
<value>المتاح</value>
|
||||
</data>
|
||||
<data name="Clean" xml:space="preserve">
|
||||
<value>نظيفة</value>
|
||||
</data>
|
||||
<data name="Clean_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد تنظيف هذا المعرض؟?</value>
|
||||
</data>
|
||||
<data name="Click_To_Upload" xml:space="preserve">
|
||||
<value>انقر هنا لاختيار الملفات</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>اقترب</value>
|
||||
</data>
|
||||
<data name="Create" xml:space="preserve">
|
||||
<value>الإبداع</value>
|
||||
</data>
|
||||
<data name="Danger_Zone" xml:space="preserve">
|
||||
<value>منطقة الخطر</value>
|
||||
</data>
|
||||
<data name="Default" xml:space="preserve">
|
||||
<value>التقصير</value>
|
||||
</data>
|
||||
<data name="Delete" xml:space="preserve">
|
||||
<value>تحذف</value>
|
||||
</data>
|
||||
<data name="Delete_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد حذف هذا المعرض؟?</value>
|
||||
</data>
|
||||
<data name="Destructive_Action_Performed" xml:space="preserve">
|
||||
<value>الإجراءات المدمرة</value>
|
||||
</data>
|
||||
<data name="Download_Gallery_Not_Allowed" xml:space="preserve">
|
||||
<value>لا يسمح لك بتحميل هذا المعرض</value>
|
||||
</data>
|
||||
<data name="Drag_And_Drop" xml:space="preserve">
|
||||
<value>Drag</value>
|
||||
</data>
|
||||
<data name="Duplicate_Item_Detected" xml:space="preserve">
|
||||
<value>ملف مزدوج مكتشف</value>
|
||||
</data>
|
||||
<data name="Export" xml:space="preserve">
|
||||
<value>الصادرات</value>
|
||||
</data>
|
||||
<data name="Failed_Add_Gallery" xml:space="preserve">
|
||||
<value>فشل في خلق المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Add_User" xml:space="preserve">
|
||||
<value>فشل في خلق مستعمل</value>
|
||||
</data>
|
||||
<data name="Failed_Delete_Gallery" xml:space="preserve">
|
||||
<value>فشل في حذف المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Delete_User" xml:space="preserve">
|
||||
<value>عدم حذف المستخدم</value>
|
||||
</data>
|
||||
<data name="Failed_Download_Gallery" xml:space="preserve">
|
||||
<value>فشل في تنزيل المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Edit_Gallery" xml:space="preserve">
|
||||
<value>فشل في تحديث المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Edit_User" xml:space="preserve">
|
||||
<value>لم يستكمل المستعمل</value>
|
||||
</data>
|
||||
<data name="Failed_Export" xml:space="preserve">
|
||||
<value>الفشل في الحصول على بيانات التصدير</value>
|
||||
</data>
|
||||
<data name="Failed_Finding_File" xml:space="preserve">
|
||||
<value>فشل في العثور على ملف</value>
|
||||
</data>
|
||||
<data name="Failed_Import" xml:space="preserve">
|
||||
<value>الفشل في الحصول على بيانات الاستيراد</value>
|
||||
</data>
|
||||
<data name="Failed_Reviewing_Media" xml:space="preserve">
|
||||
<value>عدم استعراض البند</value>
|
||||
</data>
|
||||
<data name="Failed_Update_Setting" xml:space="preserve">
|
||||
<value>فشل في تحديث الأطر</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Galleries" xml:space="preserve">
|
||||
<value>فشل في مسح المغالي</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Gallery" xml:space="preserve">
|
||||
<value>فشل في محو المعرض</value>
|
||||
</data>
|
||||
<data name="FFMPEG_Download_Failed" xml:space="preserve">
|
||||
<value>فشل في تحميل FFMPEG إلى الطريق</value>
|
||||
</data>
|
||||
<data name="FFMPEG_Downloading" xml:space="preserve">
|
||||
<value>محاولاً توليد إبهام بالفيديو، لكن شركة FMPEG لم تستكمل التحميل بعد. إذا كانت هذه تركيبة جديدة، يرجى الانتظار لتنزيلها لإكمال وضياع الإبهام</value>
|
||||
</data>
|
||||
<data name="File_Upload_Failed" xml:space="preserve">
|
||||
<value>فشل في رفع الملف</value>
|
||||
</data>
|
||||
<data name="Filter" xml:space="preserve">
|
||||
<value>فيلم</value>
|
||||
</data>
|
||||
<data name="Galleries" xml:space="preserve">
|
||||
<value>Galleries</value>
|
||||
</data>
|
||||
<data name="Gallery" xml:space="preserve">
|
||||
<value>Gallery</value>
|
||||
</data>
|
||||
<data name="Gallery_Does_Not_Exist" xml:space="preserve">
|
||||
<value>The requested gallery was not found or does not exist</value>
|
||||
</data>
|
||||
<data name="Gallery_Empty" xml:space="preserve">
|
||||
<value>لا توجد حاليا ذكريات مشتركة</value>
|
||||
</data>
|
||||
<data name="Gallery_Empty_Upload" xml:space="preserve">
|
||||
<value>لا توجد حالياً ذكريات مشتركة لماذا لا تحملين ذكرياتك!</value>
|
||||
</data>
|
||||
<data name="Gallery_Full" xml:space="preserve">
|
||||
<value>المعرض ممتلئ وقد تجاوز الحد الأقصى لحجم</value>
|
||||
</data>
|
||||
<data name="Gallery_Key" xml:space="preserve">
|
||||
<value>مفتاح المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Key_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل المفتاح السري.</value>
|
||||
</data>
|
||||
<data name="Gallery_Key_Placeholder" xml:space="preserve">
|
||||
<value>يوم الزفاف</value>
|
||||
</data>
|
||||
<data name="Gallery_Limit_Reached" xml:space="preserve">
|
||||
<value>تم الوصول إلى الحد الأقصى المسموح به من المغالي</value>
|
||||
</data>
|
||||
<data name="Gallery_List_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على قائمة المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Name" xml:space="preserve">
|
||||
<value>اسم المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Already_Exists" xml:space="preserve">
|
||||
<value>اسم المعرض موجود بالفعل</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Help" xml:space="preserve">
|
||||
<value>رجاءً أدخلي اسم المعرض الذي تريدين زيارته.</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Placeholder" xml:space="preserve">
|
||||
<value>حفل زفافي</value>
|
||||
</data>
|
||||
<data name="Gallery_Selector_Title" xml:space="preserve">
|
||||
<value>Gallery Selector</value>
|
||||
</data>
|
||||
<data name="Generate" xml:space="preserve">
|
||||
<value>الجيل</value>
|
||||
</data>
|
||||
<data name="Group" xml:space="preserve">
|
||||
<value>المجموعة</value>
|
||||
</data>
|
||||
<data name="Homepage_Load_Error" xml:space="preserve">
|
||||
<value>وقع خطأ في تحميل الصفحة الرئيسية</value>
|
||||
</data>
|
||||
<data name="Identity_Session_Error" xml:space="preserve">
|
||||
<value>عدم تحديد جلسة هوية المستخدمين - الاسم</value>
|
||||
</data>
|
||||
<data name="Image_Upload_Failed" xml:space="preserve">
|
||||
<value>فشل في تحميل الصور</value>
|
||||
</data>
|
||||
<data name="Import" xml:space="preserve">
|
||||
<value>الواردات</value>
|
||||
</data>
|
||||
<data name="Invalid_Access_Token" xml:space="preserve">
|
||||
<value>The provided access token was invalid</value>
|
||||
</data>
|
||||
<data name="Invalid_File_Type" xml:space="preserve">
|
||||
<value>نوع الملف غير صحيح</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Id" xml:space="preserve">
|
||||
<value>معرض غير رسمي Id</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Key" xml:space="preserve">
|
||||
<value>مفتاح المعرض الفاشل</value>
|
||||
</data>
|
||||
<data name="Invalid_Secret_Key_Warning" xml:space="preserve">
|
||||
<value>قُدم طلب باستخدام مفتاح سري غير صحيح</value>
|
||||
</data>
|
||||
<data name="Key" xml:space="preserve">
|
||||
<value>المفتاح</value>
|
||||
</data>
|
||||
<data name="Link" xml:space="preserve">
|
||||
<value>Link</value>
|
||||
</data>
|
||||
<data name="Login" xml:space="preserve">
|
||||
<value>Login</value>
|
||||
</data>
|
||||
<data name="Login_Failed" xml:space="preserve">
|
||||
<value>فشل في تسجيل المستخدمين</value>
|
||||
</data>
|
||||
<data name="Login_Password_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل كلمة السر.</value>
|
||||
</data>
|
||||
<data name="Login_Password_Placeholder" xml:space="preserve">
|
||||
<value>كلمة السر</value>
|
||||
</data>
|
||||
<data name="Login_Username_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل اسم المستخدم.</value>
|
||||
</data>
|
||||
<data name="Login_Username_Placeholder" xml:space="preserve">
|
||||
<value>Admin</value>
|
||||
</data>
|
||||
<data name="Logout" xml:space="preserve">
|
||||
<value>Logout</value>
|
||||
</data>
|
||||
<data name="Manage_Data" xml:space="preserve">
|
||||
<value>بيانات إدارة</value>
|
||||
</data>
|
||||
<data name="Max_File_Size" xml:space="preserve">
|
||||
<value>حجم ملف (ماكس)</value>
|
||||
</data>
|
||||
<data name="MultiFactor_Token_Set_Failed" xml:space="preserve">
|
||||
<value>فشل في إنشاء 2FA</value>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Name_Cannot_Be_Blank" xml:space="preserve">
|
||||
<value>اسم المعرض لا يمكن أن يكون فارغا</value>
|
||||
</data>
|
||||
<data name="New_Items_Pending_Review" xml:space="preserve">
|
||||
<value>البنود الجديدة قيد الاستعراض</value>
|
||||
</data>
|
||||
<data name="No_Data" xml:space="preserve">
|
||||
<value>لا توجد بيانات</value>
|
||||
</data>
|
||||
<data name="No_Files_For_Upload" xml:space="preserve">
|
||||
<value>No files were detected for upload</value>
|
||||
</data>
|
||||
<data name="No_Galleries" xml:space="preserve">
|
||||
<value>لا توجد مسابقات بعد</value>
|
||||
</data>
|
||||
<data name="No_Pending_Uploads" xml:space="preserve">
|
||||
<value>No uploads pending review</value>
|
||||
</data>
|
||||
<data name="Pending" xml:space="preserve">
|
||||
<value>معلقة</value>
|
||||
</data>
|
||||
<data name="Pending_Uploads" xml:space="preserve">
|
||||
<value>تحميل</value>
|
||||
</data>
|
||||
<data name="Pending_Uploads_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على تحميل</value>
|
||||
</data>
|
||||
<data name="Reject" xml:space="preserve">
|
||||
<value>Reject</value>
|
||||
</data>
|
||||
<data name="Rename" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Review" xml:space="preserve">
|
||||
<value>الاستعراض</value>
|
||||
</data>
|
||||
<data name="Save_QR" xml:space="preserve">
|
||||
<value>أنقذوا (كيو آر)</value>
|
||||
</data>
|
||||
<data name="Save_To_Gallery_Failed" xml:space="preserve">
|
||||
<value>فشل في توفير الصور للمعرض</value>
|
||||
</data>
|
||||
<data name="Scan_To_Share" xml:space="preserve">
|
||||
<value>قوموا بمسح شفرة (كيو آر) بهواتفكم لتشاركوا صوركم</value>
|
||||
</data>
|
||||
<data name="Settings" xml:space="preserve">
|
||||
<value>الترتيبات</value>
|
||||
</data>
|
||||
<data name="Settings_Account" xml:space="preserve">
|
||||
<value>الحساب</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Attempts_Help" xml:space="preserve">
|
||||
<value>كم عدد قطع الأشجار الفاشلة قبل أن يغلق المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Attempts_Label" xml:space="preserve">
|
||||
<value>أعمال الضبط</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Mins_Help" xml:space="preserve">
|
||||
<value>كم من الوقت يجب أن يغلق المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Mins_Label" xml:space="preserve">
|
||||
<value>أجهزة المراقبة</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Profile_Icon_Help" xml:space="preserve">
|
||||
<value>إذا ظهرت الصورة في الرأس</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Profile_Icon_Label" xml:space="preserve">
|
||||
<value>Profile Icon</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts" xml:space="preserve">
|
||||
<value>إنذار</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Account_Lockout_Help" xml:space="preserve">
|
||||
<value>يجب أن ترسل التحذيرات عندما تكون الحسابات مغلقة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Account_Lockout_Label" xml:space="preserve">
|
||||
<value>حساب القفل</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Destructive_Action_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات عند القيام بعمل تدميري</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Destructive_Action_Label" xml:space="preserve">
|
||||
<value>الأعمال المدمرة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Failed_Login_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات في محاولات قطع الأشجار الفاشلة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Failed_Login_Label" xml:space="preserve">
|
||||
<value>Failed Login</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Pending_Review_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات إذا كان هناك بند جديد قيد الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Pending_Review_Label" xml:space="preserve">
|
||||
<value>استعراض انتظار</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices" xml:space="preserve">
|
||||
<value>خدمات المعلومات الأساسية</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_Cleanup_Help" xml:space="preserve">
|
||||
<value>تنظيف الملفات المؤقتة</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_Cleanup_Label" xml:space="preserve">
|
||||
<value>التنظيف</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_DirectoryScanner_Help" xml:space="preserve">
|
||||
<value>التقطت الملفات مباشرة إلى ملف المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_DirectoryScanner_Label" xml:space="preserve">
|
||||
<value>Directory Scanner</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_EmailReport_Help" xml:space="preserve">
|
||||
<value>أرسل تقرير عن حالة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_EmailReport_Label" xml:space="preserve">
|
||||
<value>Email Reports</value>
|
||||
</data>
|
||||
<data name="Settings_Basic" xml:space="preserve">
|
||||
<value>أساسي</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_BaseUrl_Help" xml:space="preserve">
|
||||
<value>قاعدة أورل التي ستستخدم في الوصلات والإخطارات</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_BaseUrl_Label" xml:space="preserve">
|
||||
<value>القاعدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_EmailReport_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال تقرير بريد إلكتروني مع إحصائيات المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_EmailReport_Label" xml:space="preserve">
|
||||
<value>Email Report</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ForceHttps_Help" xml:space="preserve">
|
||||
<value>يجب أن Htp users be forced to https</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ForceHttps_Label" xml:space="preserve">
|
||||
<value>القوة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorDropdown_Help" xml:space="preserve">
|
||||
<value>كيف يختار المستعملون معرضاً</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorDropdown_Label" xml:space="preserve">
|
||||
<value>Gallery Selector النوع</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorHideDefault_Help" xml:space="preserve">
|
||||
<value>إذا كان خيار المعرض الافتراضي واضح</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorHideDefault_Label" xml:space="preserve">
|
||||
<value>معرض الدفن</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GuestGalleryCreation_Help" xml:space="preserve">
|
||||
<value>هل يُسمح للضيوف أن يخلقوا مجالات جديدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GuestGalleryCreation_Label" xml:space="preserve">
|
||||
<value>مهرجان الضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HideKeyFromQRCode_Help" xml:space="preserve">
|
||||
<value>إذا أُدرج المفتاح السرّي في رمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HideKeyFromQRCode_Label" xml:space="preserve">
|
||||
<value>Insecure QR Codes</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HomeLink_Help" xml:space="preserve">
|
||||
<value>يَجِبُ أَنْ يَنْقرَ الرابطَ المنزليَ إعادة توجيه المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HomeLink_Label" xml:space="preserve">
|
||||
<value>Clickable Home Link</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_LinksOpenNewTab_Help" xml:space="preserve">
|
||||
<value>يجب أن تُفتح وصلات جديدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_LinksOpenNewTab_Label" xml:space="preserve">
|
||||
<value>New Tab Links</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_MaxGalleryCount_Help" xml:space="preserve">
|
||||
<value>كم عدد المجرات المسموح للمستعملين بصنعها</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_MaxGalleryCount_Label" xml:space="preserve">
|
||||
<value>ماكس غاليري كونت</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_SingleGalleryMode_Help" xml:space="preserve">
|
||||
<value>يجب أن يكون هناك مهر واحد أو أكثر</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_SingleGalleryMode_Label" xml:space="preserve">
|
||||
<value>Gallery Mode</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ThumbnailSize_Help" xml:space="preserve">
|
||||
<value>الحد الأقصى للبكازات في أي اتجاه للإبهام</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ThumbnailSize_Label" xml:space="preserve">
|
||||
<value>حل الإبهام</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery" xml:space="preserve">
|
||||
<value>التخلف عن الدفع</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_AllowedFileTypes_Help" xml:space="preserve">
|
||||
<value>ما هو نوع الملف المسموح به للضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_AllowedFileTypes_Label" xml:space="preserve">
|
||||
<value>أنواع الملفات المسموح بها</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_BannerImage_Help" xml:space="preserve">
|
||||
<value>صورة لعرضها على قمة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_BannerImage_Label" xml:space="preserve">
|
||||
<value>طراز Banner Image</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Columns_Help" xml:space="preserve">
|
||||
<value>عدد الأعمدة المعروضة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Columns_Label" xml:space="preserve">
|
||||
<value>Column count</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_DefaultView_Help" xml:space="preserve">
|
||||
<value>ما هو مشهد قافلة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_DefaultView_Label" xml:space="preserve">
|
||||
<value>الصورة الافتراضية</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Download_Help" xml:space="preserve">
|
||||
<value>هل يسمح للضيوف بتحميل المواد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Download_Label" xml:space="preserve">
|
||||
<value>السماح بتحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_FullWidth_Help" xml:space="preserve">
|
||||
<value>عرض المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_FullWidth_Label" xml:space="preserve">
|
||||
<value>نوع العرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_IdleRefreshMins_Help" xml:space="preserve">
|
||||
<value>بعد كم من الوقت يجب أن ننعش المعرض للحصول على محتوى جديد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_IdleRefreshMins_Label" xml:space="preserve">
|
||||
<value>Idle Refresh Min</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ItemsPerPage_Help" xml:space="preserve">
|
||||
<value>العدد الأقصى للأصناف المراد عرضها في الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ItemsPerPage_Label" xml:space="preserve">
|
||||
<value>البنود الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxFileSizeMb_Help" xml:space="preserve">
|
||||
<value>ما هو أكبر في MB ملف محمول يمكن أن يكون</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxFileSizeMb_Label" xml:space="preserve">
|
||||
<value>Max File Size (MB)</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxSizeMb_Help" xml:space="preserve">
|
||||
<value>كم هو كبير في إم بي هو المعرض المسموح له أن ينمو</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxSizeMb_Label" xml:space="preserve">
|
||||
<value>مقاس ماكس غالاري</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_PreventDuplicates_Help" xml:space="preserve">
|
||||
<value>في حالة رفض ازدواجية البنود تلقائياً</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_PreventDuplicates_Label" xml:space="preserve">
|
||||
<value>منع الازدواجية</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultSort_Help" xml:space="preserve">
|
||||
<value>مرة واحدة مسح ما هو النظام</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultSort_Label" xml:space="preserve">
|
||||
<value>Default QR Sort</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultView_Help" xml:space="preserve">
|
||||
<value>مرة واحدة مسح ما التصميم إذا رأى المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultView_Label" xml:space="preserve">
|
||||
<value>Default QR View</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeEnabled_Help" xml:space="preserve">
|
||||
<value>إذا تم عرض رمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeEnabled_Label" xml:space="preserve">
|
||||
<value>يُظهرُ الرمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Quote_Help" xml:space="preserve">
|
||||
<value>اقتباس للعرض على قمة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Quote_Label" xml:space="preserve">
|
||||
<value>Quote</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RequireReview_Help" xml:space="preserve">
|
||||
<value>هل يتعين استعراض البنود قبل أن تظهر في الموقع</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RequireReview_Label" xml:space="preserve">
|
||||
<value>الاستعراض المطلوب</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RetainRejectedItems_Help" xml:space="preserve">
|
||||
<value>ينبغي تخزين الأصناف المرفوضة لاستخدامها في وقت لاحق</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RetainRejectedItems_Label" xml:space="preserve">
|
||||
<value>Retain Rejected items</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ReviewCounter_Help" xml:space="preserve">
|
||||
<value>في حالة عرض مكتب الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ReviewCounter_Label" xml:space="preserve">
|
||||
<value>الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_SecretKey_Help" xml:space="preserve">
|
||||
<value>كلمة سر لفتح المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_SecretKey_Label" xml:space="preserve">
|
||||
<value>المفتاح السري</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Upload_Help" xml:space="preserve">
|
||||
<value>هل يسمح للضيوف بتحميل المواد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Upload_Label" xml:space="preserve">
|
||||
<value>السماح بتحميلات</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_UploadPeriod_Help" xml:space="preserve">
|
||||
<value>تقييد الحمولات لفترة محددة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_UploadPeriod_Label" xml:space="preserve">
|
||||
<value>فترات تحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify" xml:space="preserve">
|
||||
<value>التحصيل</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Enabled_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال إنذارات نتيفي</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Endpoint_Help" xml:space="preserve">
|
||||
<value>عنوان الخادم</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Endpoint_Label" xml:space="preserve">
|
||||
<value>نقطة النهاية</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Priority_Help" xml:space="preserve">
|
||||
<value>قيمة الإنذارات الهامة</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Priority_Label" xml:space="preserve">
|
||||
<value>الأولوية</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Token_Help" xml:space="preserve">
|
||||
<value>المركب المستخدم للتوثيق</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Token_Label" xml:space="preserve">
|
||||
<value>Access Token</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck" xml:space="preserve">
|
||||
<value>التحقق من الهوية</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Enabled_Help" xml:space="preserve">
|
||||
<value>المستعملون الفوريون لإسمهم</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_PageLoad_Help" xml:space="preserve">
|
||||
<value>إذا كان فحص الهوية يُعرض في الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_PageLoad_Label" xml:space="preserve">
|
||||
<value>الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Upload_Help" xml:space="preserve">
|
||||
<value>إذا طُلب التحقق من الهوية لتحميل وسائط الإعلام</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Upload_Label" xml:space="preserve">
|
||||
<value>تحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Languages" xml:space="preserve">
|
||||
<value>اللغات</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Default_Help" xml:space="preserve">
|
||||
<value>:: لغة الموقع الشبكي الافتراضي إذا لم يُختار أحد</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Default_Label" xml:space="preserve">
|
||||
<value>اللغة الافتراضية</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Enabled_Help" xml:space="preserve">
|
||||
<value>السماح للمستعملين بتغيير اللغة</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy" xml:space="preserve">
|
||||
<value>Ntfy</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Enabled_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال إنذارات نتيفي</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Endpoint_Help" xml:space="preserve">
|
||||
<value>عنوان الخادم</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Endpoint_Label" xml:space="preserve">
|
||||
<value>نقطة النهاية</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Priority_Help" xml:space="preserve">
|
||||
<value>قيمة الإنذارات الهامة</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Priority_Label" xml:space="preserve">
|
||||
<value>الأولوية</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Token_Help" xml:space="preserve">
|
||||
<value>المركب المستخدم للتوثيق</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Token_Label" xml:space="preserve">
|
||||
<value>Access Token</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Topic_Help" xml:space="preserve">
|
||||
<value>موضوع إرسال إنذارات</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Topic_Label" xml:space="preserve">
|
||||
<value>الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow" xml:space="preserve">
|
||||
<value>Slideshow</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Fade_Help" xml:space="preserve">
|
||||
<value>كم من الوقت يجب أن يكون الانتقال بين الشرائح</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Fade_Label" xml:space="preserve">
|
||||
<value>Fade</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Interval_Help" xml:space="preserve">
|
||||
<value>كم من الوقت في الثواني يجب أن ينزلق قبل الانتقال إلى الشريحة التالية</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Interval_Label" xml:space="preserve">
|
||||
<value>الفترات الفاصلة</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Limit_Help" xml:space="preserve">
|
||||
<value>الحد الأقصى لعدد الشرائح التي ستدرج</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Limit_Label" xml:space="preserve">
|
||||
<value>Limit</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Share_Slide_Help" xml:space="preserve">
|
||||
<value>هل يجب أن تُرفض شفرة (كيو آر) في النهاية للضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Share_Slide_Label" xml:space="preserve">
|
||||
<value>Include Share Slide</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp" xml:space="preserve">
|
||||
<value>Email</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Display_Name_Help" xml:space="preserve">
|
||||
<value>اسم المرسل الصديق للمستعمل</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Display_Name_Label" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Enabled_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال تنبيهات بالبريد الإلكتروني</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_From_Help" xml:space="preserve">
|
||||
<value>عنوان البريد الإلكتروني أرسل من</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_From_Label" xml:space="preserve">
|
||||
<value>من</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Host_Help" xml:space="preserve">
|
||||
<value>The hostname of the SMTP provider</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Host_Label" xml:space="preserve">
|
||||
<value>المضيفة</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Password_Help" xml:space="preserve">
|
||||
<value>كلمة السر المستخدمة لتوثيق الطلب</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Password_Label" xml:space="preserve">
|
||||
<value>كلمة السر</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Port_Help" xml:space="preserve">
|
||||
<value>The port used by the SMTP provider</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Port_Label" xml:space="preserve">
|
||||
<value>الميناء</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Recipient_Help" xml:space="preserve">
|
||||
<value>قائمة بريد إلكتروني منفصلة</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Recipient_Label" xml:space="preserve">
|
||||
<value>المتلقين</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Use_SSL_Help" xml:space="preserve">
|
||||
<value>هل يستخدم مقدم خدمات SMTP SSL</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Use_SSL_Label" xml:space="preserve">
|
||||
<value>Use SSL</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Username_Help" xml:space="preserve">
|
||||
<value>الاسم المستخدم لتوثيق الطلب</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Username_Label" xml:space="preserve">
|
||||
<value>المستعمل</value>
|
||||
</data>
|
||||
<data name="Settings_Themes" xml:space="preserve">
|
||||
<value>المواضيع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Default_Help" xml:space="preserve">
|
||||
<value>الموضوع الرئيسي للموقع إذا لم يُختار أحد</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Default_Label" xml:space="preserve">
|
||||
<value>التقصير الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Enabled_Help" xml:space="preserve">
|
||||
<value>السماح للمستعملين بتغيير الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Share_Code" xml:space="preserve">
|
||||
<value>Share Code</value>
|
||||
</data>
|
||||
<data name="Sort" xml:space="preserve">
|
||||
<value>Sort</value>
|
||||
</data>
|
||||
<data name="Space_Percent_Used" xml:space="preserve">
|
||||
<value>Usage</value>
|
||||
</data>
|
||||
<data name="Total" xml:space="preserve">
|
||||
<value>المجموع</value>
|
||||
</data>
|
||||
<data name="Unknown_Review_Action" xml:space="preserve">
|
||||
<value>إجراء استعراض غير معروف</value>
|
||||
</data>
|
||||
<data name="Update" xml:space="preserve">
|
||||
<value>آخر</value>
|
||||
</data>
|
||||
<data name="Upload_Media" xml:space="preserve">
|
||||
<value>تحميل وسائط الإعلام</value>
|
||||
</data>
|
||||
<data name="Uploaded_By" xml:space="preserve">
|
||||
<value>محمولة</value>
|
||||
</data>
|
||||
<data name="Uploader" xml:space="preserve">
|
||||
<value>الحمولة</value>
|
||||
</data>
|
||||
<data name="User_CPassword_Missmatch" xml:space="preserve">
|
||||
<value>كلمة السر المقدّمة وكلمة سر الكونفريم لا تتطابق</value>
|
||||
</data>
|
||||
<data name="User_Name_Already_Exists" xml:space="preserve">
|
||||
<value>يضاف المستخدم الذي يحمل نفس الاسم موجود بالفعل</value>
|
||||
</data>
|
||||
<data name="Username" xml:space="preserve">
|
||||
<value>المستعمل</value>
|
||||
</data>
|
||||
<data name="Users" xml:space="preserve">
|
||||
<value>المستخدمون</value>
|
||||
</data>
|
||||
<data name="Users_List_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على قائمة المستخدمين</value>
|
||||
</data>
|
||||
<data name="View" xml:space="preserve">
|
||||
<value>View</value>
|
||||
</data>
|
||||
<data name="Visit" xml:space="preserve">
|
||||
<value>زيارة</value>
|
||||
</data>
|
||||
<data name="Wipe" xml:space="preserve">
|
||||
<value>Wipe</value>
|
||||
</data>
|
||||
<data name="Wipe_All_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد من أنك تريد مسح كل المجرات؟?</value>
|
||||
</data>
|
||||
<data name="Wipe_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد مسح هذا المعرض؟?</value>
|
||||
</data>
|
||||
</root>
|
||||
901
WeddingShare/Resources/Lang/Translations.ar-AE.resx
Normal file
901
WeddingShare/Resources/Lang/Translations.ar-AE.resx
Normal file
@@ -0,0 +1,901 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="400_Error_Message" xml:space="preserve">
|
||||
<value>إذا استمرت هذه المسألة، يرجى الاتصال بمدير الإنترنت</value>
|
||||
</data>
|
||||
<data name="400_Error_Title" xml:space="preserve">
|
||||
<value>يبدو أن خطأ غير متوقع حدث.</value>
|
||||
</data>
|
||||
<data name="401_Error_Message" xml:space="preserve">
|
||||
<value>يبدو أنك غير مصرح لك بالنظر إلى تلك الصفحة.</value>
|
||||
</data>
|
||||
<data name="401_Error_Title" xml:space="preserve">
|
||||
<value>Access Denied</value>
|
||||
</data>
|
||||
<data name="402_Error_Message" xml:space="preserve">
|
||||
<value>المفتاح السري كان غير صحيح</value>
|
||||
</data>
|
||||
<data name="402_Error_Title" xml:space="preserve">
|
||||
<value>مفتاح سرّي غير مستقر</value>
|
||||
</data>
|
||||
<data name="403_Error_Message" xml:space="preserve">
|
||||
<value>المجوهرات المحددة لا وجود لها وليس لديك إذن لخلقها</value>
|
||||
</data>
|
||||
<data name="403_Error_Title" xml:space="preserve">
|
||||
<value>Access Denied</value>
|
||||
</data>
|
||||
<data name="405_Error_Message" xml:space="preserve">
|
||||
<value>لقد وصلت إلى الحد الأقصى لخلق المعرض</value>
|
||||
</data>
|
||||
<data name="405_Error_Title" xml:space="preserve">
|
||||
<value>Limit Reached</value>
|
||||
</data>
|
||||
<data name="406_Error_Message" xml:space="preserve">
|
||||
<value>المعرض المحدد لا يوجد</value>
|
||||
</data>
|
||||
<data name="406_Error_Title" xml:space="preserve">
|
||||
<value>جالري</value>
|
||||
</data>
|
||||
<data name="Actions" xml:space="preserve">
|
||||
<value>الإجراءات</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>مضافا إليها</value>
|
||||
</data>
|
||||
<data name="Approve" xml:space="preserve">
|
||||
<value>الموافقة</value>
|
||||
</data>
|
||||
<data name="Approved" xml:space="preserve">
|
||||
<value>الوظائف المعتمدة</value>
|
||||
</data>
|
||||
<data name="Available_Galleries" xml:space="preserve">
|
||||
<value>المتاح</value>
|
||||
</data>
|
||||
<data name="Clean" xml:space="preserve">
|
||||
<value>نظيفة</value>
|
||||
</data>
|
||||
<data name="Clean_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد تنظيف هذا المعرض؟?</value>
|
||||
</data>
|
||||
<data name="Click_To_Upload" xml:space="preserve">
|
||||
<value>انقر هنا لاختيار الملفات</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>اقترب</value>
|
||||
</data>
|
||||
<data name="Create" xml:space="preserve">
|
||||
<value>الإبداع</value>
|
||||
</data>
|
||||
<data name="Danger_Zone" xml:space="preserve">
|
||||
<value>منطقة الخطر</value>
|
||||
</data>
|
||||
<data name="Default" xml:space="preserve">
|
||||
<value>التقصير</value>
|
||||
</data>
|
||||
<data name="Delete" xml:space="preserve">
|
||||
<value>تحذف</value>
|
||||
</data>
|
||||
<data name="Delete_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد حذف هذا المعرض؟?</value>
|
||||
</data>
|
||||
<data name="Destructive_Action_Performed" xml:space="preserve">
|
||||
<value>الإجراءات المدمرة</value>
|
||||
</data>
|
||||
<data name="Download_Gallery_Not_Allowed" xml:space="preserve">
|
||||
<value>لا يسمح لك بتحميل هذا المعرض</value>
|
||||
</data>
|
||||
<data name="Drag_And_Drop" xml:space="preserve">
|
||||
<value>Drag</value>
|
||||
</data>
|
||||
<data name="Duplicate_Item_Detected" xml:space="preserve">
|
||||
<value>ملف مزدوج مكتشف</value>
|
||||
</data>
|
||||
<data name="Export" xml:space="preserve">
|
||||
<value>الصادرات</value>
|
||||
</data>
|
||||
<data name="Failed_Add_Gallery" xml:space="preserve">
|
||||
<value>فشل في خلق المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Add_User" xml:space="preserve">
|
||||
<value>فشل في خلق مستعمل</value>
|
||||
</data>
|
||||
<data name="Failed_Delete_Gallery" xml:space="preserve">
|
||||
<value>فشل في حذف المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Delete_User" xml:space="preserve">
|
||||
<value>عدم حذف المستخدم</value>
|
||||
</data>
|
||||
<data name="Failed_Download_Gallery" xml:space="preserve">
|
||||
<value>فشل في تنزيل المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Edit_Gallery" xml:space="preserve">
|
||||
<value>فشل في تحديث المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Edit_User" xml:space="preserve">
|
||||
<value>لم يستكمل المستعمل</value>
|
||||
</data>
|
||||
<data name="Failed_Export" xml:space="preserve">
|
||||
<value>الفشل في الحصول على بيانات التصدير</value>
|
||||
</data>
|
||||
<data name="Failed_Finding_File" xml:space="preserve">
|
||||
<value>فشل في العثور على ملف</value>
|
||||
</data>
|
||||
<data name="Failed_Import" xml:space="preserve">
|
||||
<value>الفشل في الحصول على بيانات الاستيراد</value>
|
||||
</data>
|
||||
<data name="Failed_Reviewing_Media" xml:space="preserve">
|
||||
<value>عدم استعراض البند</value>
|
||||
</data>
|
||||
<data name="Failed_Update_Setting" xml:space="preserve">
|
||||
<value>فشل في تحديث الأطر</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Galleries" xml:space="preserve">
|
||||
<value>فشل في مسح المغالي</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Gallery" xml:space="preserve">
|
||||
<value>فشل في محو المعرض</value>
|
||||
</data>
|
||||
<data name="FFMPEG_Download_Failed" xml:space="preserve">
|
||||
<value>فشل في تحميل FFMPEG إلى الطريق</value>
|
||||
</data>
|
||||
<data name="FFMPEG_Downloading" xml:space="preserve">
|
||||
<value>محاولاً توليد إبهام بالفيديو، لكن شركة FMPEG لم تستكمل التحميل بعد. إذا كانت هذه تركيبة جديدة، يرجى الانتظار لتنزيلها لإكمال وضياع الإبهام</value>
|
||||
</data>
|
||||
<data name="File_Upload_Failed" xml:space="preserve">
|
||||
<value>فشل في رفع الملف</value>
|
||||
</data>
|
||||
<data name="Filter" xml:space="preserve">
|
||||
<value>فيلم</value>
|
||||
</data>
|
||||
<data name="Galleries" xml:space="preserve">
|
||||
<value>Galleries</value>
|
||||
</data>
|
||||
<data name="Gallery" xml:space="preserve">
|
||||
<value>Gallery</value>
|
||||
</data>
|
||||
<data name="Gallery_Does_Not_Exist" xml:space="preserve">
|
||||
<value>The requested gallery was not found or does not exist</value>
|
||||
</data>
|
||||
<data name="Gallery_Empty" xml:space="preserve">
|
||||
<value>لا توجد حاليا ذكريات مشتركة</value>
|
||||
</data>
|
||||
<data name="Gallery_Empty_Upload" xml:space="preserve">
|
||||
<value>لا توجد حالياً ذكريات مشتركة لماذا لا تحملين ذكرياتك!</value>
|
||||
</data>
|
||||
<data name="Gallery_Full" xml:space="preserve">
|
||||
<value>المعرض ممتلئ وقد تجاوز الحد الأقصى لحجم</value>
|
||||
</data>
|
||||
<data name="Gallery_Key" xml:space="preserve">
|
||||
<value>مفتاح المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Key_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل المفتاح السري.</value>
|
||||
</data>
|
||||
<data name="Gallery_Key_Placeholder" xml:space="preserve">
|
||||
<value>يوم الزفاف</value>
|
||||
</data>
|
||||
<data name="Gallery_Limit_Reached" xml:space="preserve">
|
||||
<value>تم الوصول إلى الحد الأقصى المسموح به من المغالي</value>
|
||||
</data>
|
||||
<data name="Gallery_List_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على قائمة المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Name" xml:space="preserve">
|
||||
<value>اسم المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Already_Exists" xml:space="preserve">
|
||||
<value>اسم المعرض موجود بالفعل</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Help" xml:space="preserve">
|
||||
<value>رجاءً أدخلي اسم المعرض الذي تريدين زيارته.</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Placeholder" xml:space="preserve">
|
||||
<value>حفل زفافي</value>
|
||||
</data>
|
||||
<data name="Gallery_Selector_Title" xml:space="preserve">
|
||||
<value>Gallery Selector</value>
|
||||
</data>
|
||||
<data name="Generate" xml:space="preserve">
|
||||
<value>الجيل</value>
|
||||
</data>
|
||||
<data name="Group" xml:space="preserve">
|
||||
<value>المجموعة</value>
|
||||
</data>
|
||||
<data name="Homepage_Load_Error" xml:space="preserve">
|
||||
<value>وقع خطأ في تحميل الصفحة الرئيسية</value>
|
||||
</data>
|
||||
<data name="Identity_Session_Error" xml:space="preserve">
|
||||
<value>عدم تحديد جلسة هوية المستخدمين - الاسم</value>
|
||||
</data>
|
||||
<data name="Image_Upload_Failed" xml:space="preserve">
|
||||
<value>فشل في تحميل الصور</value>
|
||||
</data>
|
||||
<data name="Import" xml:space="preserve">
|
||||
<value>الواردات</value>
|
||||
</data>
|
||||
<data name="Invalid_Access_Token" xml:space="preserve">
|
||||
<value>The provided access token was invalid</value>
|
||||
</data>
|
||||
<data name="Invalid_File_Type" xml:space="preserve">
|
||||
<value>نوع الملف غير صحيح</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Id" xml:space="preserve">
|
||||
<value>معرض غير رسمي Id</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Key" xml:space="preserve">
|
||||
<value>مفتاح المعرض الفاشل</value>
|
||||
</data>
|
||||
<data name="Invalid_Secret_Key_Warning" xml:space="preserve">
|
||||
<value>قُدم طلب باستخدام مفتاح سري غير صحيح</value>
|
||||
</data>
|
||||
<data name="Key" xml:space="preserve">
|
||||
<value>المفتاح</value>
|
||||
</data>
|
||||
<data name="Link" xml:space="preserve">
|
||||
<value>Link</value>
|
||||
</data>
|
||||
<data name="Login" xml:space="preserve">
|
||||
<value>Login</value>
|
||||
</data>
|
||||
<data name="Login_Failed" xml:space="preserve">
|
||||
<value>فشل في تسجيل المستخدمين</value>
|
||||
</data>
|
||||
<data name="Login_Password_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل كلمة السر.</value>
|
||||
</data>
|
||||
<data name="Login_Password_Placeholder" xml:space="preserve">
|
||||
<value>كلمة السر</value>
|
||||
</data>
|
||||
<data name="Login_Username_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل اسم المستخدم.</value>
|
||||
</data>
|
||||
<data name="Login_Username_Placeholder" xml:space="preserve">
|
||||
<value>Admin</value>
|
||||
</data>
|
||||
<data name="Logout" xml:space="preserve">
|
||||
<value>Logout</value>
|
||||
</data>
|
||||
<data name="Manage_Data" xml:space="preserve">
|
||||
<value>بيانات إدارة</value>
|
||||
</data>
|
||||
<data name="Max_File_Size" xml:space="preserve">
|
||||
<value>حجم ملف (ماكس)</value>
|
||||
</data>
|
||||
<data name="MultiFactor_Token_Set_Failed" xml:space="preserve">
|
||||
<value>فشل في إنشاء 2FA</value>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Name_Cannot_Be_Blank" xml:space="preserve">
|
||||
<value>اسم المعرض لا يمكن أن يكون فارغا</value>
|
||||
</data>
|
||||
<data name="New_Items_Pending_Review" xml:space="preserve">
|
||||
<value>البنود الجديدة قيد الاستعراض</value>
|
||||
</data>
|
||||
<data name="No_Data" xml:space="preserve">
|
||||
<value>لا توجد بيانات</value>
|
||||
</data>
|
||||
<data name="No_Files_For_Upload" xml:space="preserve">
|
||||
<value>No files were detected for upload</value>
|
||||
</data>
|
||||
<data name="No_Galleries" xml:space="preserve">
|
||||
<value>لا توجد مسابقات بعد</value>
|
||||
</data>
|
||||
<data name="No_Pending_Uploads" xml:space="preserve">
|
||||
<value>No uploads pending review</value>
|
||||
</data>
|
||||
<data name="Pending" xml:space="preserve">
|
||||
<value>معلقة</value>
|
||||
</data>
|
||||
<data name="Pending_Uploads" xml:space="preserve">
|
||||
<value>تحميل</value>
|
||||
</data>
|
||||
<data name="Pending_Uploads_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على تحميل</value>
|
||||
</data>
|
||||
<data name="Reject" xml:space="preserve">
|
||||
<value>Reject</value>
|
||||
</data>
|
||||
<data name="Rename" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Review" xml:space="preserve">
|
||||
<value>الاستعراض</value>
|
||||
</data>
|
||||
<data name="Save_QR" xml:space="preserve">
|
||||
<value>أنقذوا (كيو آر)</value>
|
||||
</data>
|
||||
<data name="Save_To_Gallery_Failed" xml:space="preserve">
|
||||
<value>فشل في توفير الصور للمعرض</value>
|
||||
</data>
|
||||
<data name="Scan_To_Share" xml:space="preserve">
|
||||
<value>قوموا بمسح شفرة (كيو آر) بهواتفكم لتشاركوا صوركم</value>
|
||||
</data>
|
||||
<data name="Settings" xml:space="preserve">
|
||||
<value>الترتيبات</value>
|
||||
</data>
|
||||
<data name="Settings_Account" xml:space="preserve">
|
||||
<value>الحساب</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Attempts_Help" xml:space="preserve">
|
||||
<value>كم عدد قطع الأشجار الفاشلة قبل أن يغلق المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Attempts_Label" xml:space="preserve">
|
||||
<value>أعمال الضبط</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Mins_Help" xml:space="preserve">
|
||||
<value>كم من الوقت يجب أن يغلق المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Mins_Label" xml:space="preserve">
|
||||
<value>أجهزة المراقبة</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Profile_Icon_Help" xml:space="preserve">
|
||||
<value>إذا ظهرت الصورة في الرأس</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Profile_Icon_Label" xml:space="preserve">
|
||||
<value>Profile Icon</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts" xml:space="preserve">
|
||||
<value>إنذار</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Account_Lockout_Help" xml:space="preserve">
|
||||
<value>يجب أن ترسل التحذيرات عندما تكون الحسابات مغلقة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Account_Lockout_Label" xml:space="preserve">
|
||||
<value>حساب القفل</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Destructive_Action_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات عند القيام بعمل تدميري</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Destructive_Action_Label" xml:space="preserve">
|
||||
<value>الأعمال المدمرة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Failed_Login_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات في محاولات قطع الأشجار الفاشلة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Failed_Login_Label" xml:space="preserve">
|
||||
<value>Failed Login</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Pending_Review_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات إذا كان هناك بند جديد قيد الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Pending_Review_Label" xml:space="preserve">
|
||||
<value>استعراض انتظار</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices" xml:space="preserve">
|
||||
<value>خدمات المعلومات الأساسية</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_Cleanup_Help" xml:space="preserve">
|
||||
<value>تنظيف الملفات المؤقتة</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_Cleanup_Label" xml:space="preserve">
|
||||
<value>التنظيف</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_DirectoryScanner_Help" xml:space="preserve">
|
||||
<value>التقطت الملفات مباشرة إلى ملف المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_DirectoryScanner_Label" xml:space="preserve">
|
||||
<value>Directory Scanner</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_EmailReport_Help" xml:space="preserve">
|
||||
<value>أرسل تقرير عن حالة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_EmailReport_Label" xml:space="preserve">
|
||||
<value>Email Reports</value>
|
||||
</data>
|
||||
<data name="Settings_Basic" xml:space="preserve">
|
||||
<value>أساسي</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_BaseUrl_Help" xml:space="preserve">
|
||||
<value>قاعدة أورل التي ستستخدم في الوصلات والإخطارات</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_BaseUrl_Label" xml:space="preserve">
|
||||
<value>القاعدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_EmailReport_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال تقرير بريد إلكتروني مع إحصائيات المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_EmailReport_Label" xml:space="preserve">
|
||||
<value>Email Report</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ForceHttps_Help" xml:space="preserve">
|
||||
<value>يجب أن Htp users be forced to https</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ForceHttps_Label" xml:space="preserve">
|
||||
<value>القوة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorDropdown_Help" xml:space="preserve">
|
||||
<value>كيف يختار المستعملون معرضاً</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorDropdown_Label" xml:space="preserve">
|
||||
<value>Gallery Selector النوع</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorHideDefault_Help" xml:space="preserve">
|
||||
<value>إذا كان خيار المعرض الافتراضي واضح</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorHideDefault_Label" xml:space="preserve">
|
||||
<value>معرض الدفن</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GuestGalleryCreation_Help" xml:space="preserve">
|
||||
<value>هل يُسمح للضيوف أن يخلقوا مجالات جديدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GuestGalleryCreation_Label" xml:space="preserve">
|
||||
<value>مهرجان الضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HideKeyFromQRCode_Help" xml:space="preserve">
|
||||
<value>إذا أُدرج المفتاح السرّي في رمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HideKeyFromQRCode_Label" xml:space="preserve">
|
||||
<value>Insecure QR Codes</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HomeLink_Help" xml:space="preserve">
|
||||
<value>يَجِبُ أَنْ يَنْقرَ الرابطَ المنزليَ إعادة توجيه المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HomeLink_Label" xml:space="preserve">
|
||||
<value>Clickable Home Link</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_LinksOpenNewTab_Help" xml:space="preserve">
|
||||
<value>يجب أن تُفتح وصلات جديدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_LinksOpenNewTab_Label" xml:space="preserve">
|
||||
<value>New Tab Links</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_MaxGalleryCount_Help" xml:space="preserve">
|
||||
<value>كم عدد المجرات المسموح للمستعملين بصنعها</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_MaxGalleryCount_Label" xml:space="preserve">
|
||||
<value>ماكس غاليري كونت</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_SingleGalleryMode_Help" xml:space="preserve">
|
||||
<value>يجب أن يكون هناك مهر واحد أو أكثر</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_SingleGalleryMode_Label" xml:space="preserve">
|
||||
<value>Gallery Mode</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ThumbnailSize_Help" xml:space="preserve">
|
||||
<value>الحد الأقصى للبكازات في أي اتجاه للإبهام</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ThumbnailSize_Label" xml:space="preserve">
|
||||
<value>حل الإبهام</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery" xml:space="preserve">
|
||||
<value>التخلف عن الدفع</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_AllowedFileTypes_Help" xml:space="preserve">
|
||||
<value>ما هو نوع الملف المسموح به للضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_AllowedFileTypes_Label" xml:space="preserve">
|
||||
<value>أنواع الملفات المسموح بها</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_BannerImage_Help" xml:space="preserve">
|
||||
<value>صورة لعرضها على قمة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_BannerImage_Label" xml:space="preserve">
|
||||
<value>طراز Banner Image</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Columns_Help" xml:space="preserve">
|
||||
<value>عدد الأعمدة المعروضة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Columns_Label" xml:space="preserve">
|
||||
<value>Column count</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_DefaultView_Help" xml:space="preserve">
|
||||
<value>ما هو مشهد قافلة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_DefaultView_Label" xml:space="preserve">
|
||||
<value>الصورة الافتراضية</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Download_Help" xml:space="preserve">
|
||||
<value>هل يسمح للضيوف بتحميل المواد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Download_Label" xml:space="preserve">
|
||||
<value>السماح بتحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_FullWidth_Help" xml:space="preserve">
|
||||
<value>عرض المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_FullWidth_Label" xml:space="preserve">
|
||||
<value>نوع العرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_IdleRefreshMins_Help" xml:space="preserve">
|
||||
<value>بعد كم من الوقت يجب أن ننعش المعرض للحصول على محتوى جديد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_IdleRefreshMins_Label" xml:space="preserve">
|
||||
<value>Idle Refresh Min</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ItemsPerPage_Help" xml:space="preserve">
|
||||
<value>العدد الأقصى للأصناف المراد عرضها في الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ItemsPerPage_Label" xml:space="preserve">
|
||||
<value>البنود الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxFileSizeMb_Help" xml:space="preserve">
|
||||
<value>ما هو أكبر في MB ملف محمول يمكن أن يكون</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxFileSizeMb_Label" xml:space="preserve">
|
||||
<value>Max File Size (MB)</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxSizeMb_Help" xml:space="preserve">
|
||||
<value>كم هو كبير في إم بي هو المعرض المسموح له أن ينمو</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxSizeMb_Label" xml:space="preserve">
|
||||
<value>مقاس ماكس غالاري</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_PreventDuplicates_Help" xml:space="preserve">
|
||||
<value>في حالة رفض ازدواجية البنود تلقائياً</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_PreventDuplicates_Label" xml:space="preserve">
|
||||
<value>منع الازدواجية</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultSort_Help" xml:space="preserve">
|
||||
<value>مرة واحدة مسح ما هو النظام</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultSort_Label" xml:space="preserve">
|
||||
<value>Default QR Sort</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultView_Help" xml:space="preserve">
|
||||
<value>مرة واحدة مسح ما التصميم إذا رأى المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultView_Label" xml:space="preserve">
|
||||
<value>Default QR View</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeEnabled_Help" xml:space="preserve">
|
||||
<value>إذا تم عرض رمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeEnabled_Label" xml:space="preserve">
|
||||
<value>يُظهرُ الرمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Quote_Help" xml:space="preserve">
|
||||
<value>اقتباس للعرض على قمة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Quote_Label" xml:space="preserve">
|
||||
<value>Quote</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RequireReview_Help" xml:space="preserve">
|
||||
<value>هل يتعين استعراض البنود قبل أن تظهر في الموقع</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RequireReview_Label" xml:space="preserve">
|
||||
<value>الاستعراض المطلوب</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RetainRejectedItems_Help" xml:space="preserve">
|
||||
<value>ينبغي تخزين الأصناف المرفوضة لاستخدامها في وقت لاحق</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RetainRejectedItems_Label" xml:space="preserve">
|
||||
<value>Retain Rejected items</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ReviewCounter_Help" xml:space="preserve">
|
||||
<value>في حالة عرض مكتب الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ReviewCounter_Label" xml:space="preserve">
|
||||
<value>الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_SecretKey_Help" xml:space="preserve">
|
||||
<value>كلمة سر لفتح المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_SecretKey_Label" xml:space="preserve">
|
||||
<value>المفتاح السري</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Upload_Help" xml:space="preserve">
|
||||
<value>هل يسمح للضيوف بتحميل المواد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Upload_Label" xml:space="preserve">
|
||||
<value>السماح بتحميلات</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_UploadPeriod_Help" xml:space="preserve">
|
||||
<value>تقييد الحمولات لفترة محددة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_UploadPeriod_Label" xml:space="preserve">
|
||||
<value>فترات تحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify" xml:space="preserve">
|
||||
<value>التحصيل</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Enabled_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال إنذارات نتيفي</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Endpoint_Help" xml:space="preserve">
|
||||
<value>عنوان الخادم</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Endpoint_Label" xml:space="preserve">
|
||||
<value>نقطة النهاية</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Priority_Help" xml:space="preserve">
|
||||
<value>قيمة الإنذارات الهامة</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Priority_Label" xml:space="preserve">
|
||||
<value>الأولوية</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Token_Help" xml:space="preserve">
|
||||
<value>المركب المستخدم للتوثيق</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Token_Label" xml:space="preserve">
|
||||
<value>Access Token</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck" xml:space="preserve">
|
||||
<value>التحقق من الهوية</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Enabled_Help" xml:space="preserve">
|
||||
<value>المستعملون الفوريون لإسمهم</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_PageLoad_Help" xml:space="preserve">
|
||||
<value>إذا كان فحص الهوية يُعرض في الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_PageLoad_Label" xml:space="preserve">
|
||||
<value>الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Upload_Help" xml:space="preserve">
|
||||
<value>إذا طُلب التحقق من الهوية لتحميل وسائط الإعلام</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Upload_Label" xml:space="preserve">
|
||||
<value>تحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Languages" xml:space="preserve">
|
||||
<value>اللغات</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Default_Help" xml:space="preserve">
|
||||
<value>:: لغة الموقع الشبكي الافتراضي إذا لم يُختار أحد</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Default_Label" xml:space="preserve">
|
||||
<value>اللغة الافتراضية</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Enabled_Help" xml:space="preserve">
|
||||
<value>السماح للمستعملين بتغيير اللغة</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy" xml:space="preserve">
|
||||
<value>Ntfy</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Enabled_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال إنذارات نتيفي</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Endpoint_Help" xml:space="preserve">
|
||||
<value>عنوان الخادم</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Endpoint_Label" xml:space="preserve">
|
||||
<value>نقطة النهاية</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Priority_Help" xml:space="preserve">
|
||||
<value>قيمة الإنذارات الهامة</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Priority_Label" xml:space="preserve">
|
||||
<value>الأولوية</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Token_Help" xml:space="preserve">
|
||||
<value>المركب المستخدم للتوثيق</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Token_Label" xml:space="preserve">
|
||||
<value>Access Token</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Topic_Help" xml:space="preserve">
|
||||
<value>موضوع إرسال إنذارات</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Topic_Label" xml:space="preserve">
|
||||
<value>الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow" xml:space="preserve">
|
||||
<value>Slideshow</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Fade_Help" xml:space="preserve">
|
||||
<value>كم من الوقت يجب أن يكون الانتقال بين الشرائح</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Fade_Label" xml:space="preserve">
|
||||
<value>Fade</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Interval_Help" xml:space="preserve">
|
||||
<value>كم من الوقت في الثواني يجب أن ينزلق قبل الانتقال إلى الشريحة التالية</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Interval_Label" xml:space="preserve">
|
||||
<value>الفترات الفاصلة</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Limit_Help" xml:space="preserve">
|
||||
<value>الحد الأقصى لعدد الشرائح التي ستدرج</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Limit_Label" xml:space="preserve">
|
||||
<value>Limit</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Share_Slide_Help" xml:space="preserve">
|
||||
<value>هل يجب أن تُرفض شفرة (كيو آر) في النهاية للضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Share_Slide_Label" xml:space="preserve">
|
||||
<value>Include Share Slide</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp" xml:space="preserve">
|
||||
<value>Email</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Display_Name_Help" xml:space="preserve">
|
||||
<value>اسم المرسل الصديق للمستعمل</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Display_Name_Label" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Enabled_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال تنبيهات بالبريد الإلكتروني</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_From_Help" xml:space="preserve">
|
||||
<value>عنوان البريد الإلكتروني أرسل من</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_From_Label" xml:space="preserve">
|
||||
<value>من</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Host_Help" xml:space="preserve">
|
||||
<value>The hostname of the SMTP provider</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Host_Label" xml:space="preserve">
|
||||
<value>المضيفة</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Password_Help" xml:space="preserve">
|
||||
<value>كلمة السر المستخدمة لتوثيق الطلب</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Password_Label" xml:space="preserve">
|
||||
<value>كلمة السر</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Port_Help" xml:space="preserve">
|
||||
<value>The port used by the SMTP provider</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Port_Label" xml:space="preserve">
|
||||
<value>الميناء</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Recipient_Help" xml:space="preserve">
|
||||
<value>قائمة بريد إلكتروني منفصلة</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Recipient_Label" xml:space="preserve">
|
||||
<value>المتلقين</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Use_SSL_Help" xml:space="preserve">
|
||||
<value>هل يستخدم مقدم خدمات SMTP SSL</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Use_SSL_Label" xml:space="preserve">
|
||||
<value>Use SSL</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Username_Help" xml:space="preserve">
|
||||
<value>الاسم المستخدم لتوثيق الطلب</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Username_Label" xml:space="preserve">
|
||||
<value>المستعمل</value>
|
||||
</data>
|
||||
<data name="Settings_Themes" xml:space="preserve">
|
||||
<value>المواضيع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Default_Help" xml:space="preserve">
|
||||
<value>الموضوع الرئيسي للموقع إذا لم يُختار أحد</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Default_Label" xml:space="preserve">
|
||||
<value>التقصير الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Enabled_Help" xml:space="preserve">
|
||||
<value>السماح للمستعملين بتغيير الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Share_Code" xml:space="preserve">
|
||||
<value>Share Code</value>
|
||||
</data>
|
||||
<data name="Sort" xml:space="preserve">
|
||||
<value>Sort</value>
|
||||
</data>
|
||||
<data name="Space_Percent_Used" xml:space="preserve">
|
||||
<value>Usage</value>
|
||||
</data>
|
||||
<data name="Total" xml:space="preserve">
|
||||
<value>المجموع</value>
|
||||
</data>
|
||||
<data name="Unknown_Review_Action" xml:space="preserve">
|
||||
<value>إجراء استعراض غير معروف</value>
|
||||
</data>
|
||||
<data name="Update" xml:space="preserve">
|
||||
<value>آخر</value>
|
||||
</data>
|
||||
<data name="Upload_Media" xml:space="preserve">
|
||||
<value>تحميل وسائط الإعلام</value>
|
||||
</data>
|
||||
<data name="Uploaded_By" xml:space="preserve">
|
||||
<value>محمولة</value>
|
||||
</data>
|
||||
<data name="Uploader" xml:space="preserve">
|
||||
<value>الحمولة</value>
|
||||
</data>
|
||||
<data name="User_CPassword_Missmatch" xml:space="preserve">
|
||||
<value>كلمة السر المقدّمة وكلمة سر الكونفريم لا تتطابق</value>
|
||||
</data>
|
||||
<data name="User_Name_Already_Exists" xml:space="preserve">
|
||||
<value>يضاف المستخدم الذي يحمل نفس الاسم موجود بالفعل</value>
|
||||
</data>
|
||||
<data name="Username" xml:space="preserve">
|
||||
<value>المستعمل</value>
|
||||
</data>
|
||||
<data name="Users" xml:space="preserve">
|
||||
<value>المستخدمون</value>
|
||||
</data>
|
||||
<data name="Users_List_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على قائمة المستخدمين</value>
|
||||
</data>
|
||||
<data name="View" xml:space="preserve">
|
||||
<value>View</value>
|
||||
</data>
|
||||
<data name="Visit" xml:space="preserve">
|
||||
<value>زيارة</value>
|
||||
</data>
|
||||
<data name="Wipe" xml:space="preserve">
|
||||
<value>Wipe</value>
|
||||
</data>
|
||||
<data name="Wipe_All_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد من أنك تريد مسح كل المجرات؟?</value>
|
||||
</data>
|
||||
<data name="Wipe_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد مسح هذا المعرض؟?</value>
|
||||
</data>
|
||||
</root>
|
||||
901
WeddingShare/Resources/Lang/Translations.ar-BH.resx
Normal file
901
WeddingShare/Resources/Lang/Translations.ar-BH.resx
Normal file
@@ -0,0 +1,901 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="400_Error_Message" xml:space="preserve">
|
||||
<value>إذا استمرت هذه المسألة، يرجى الاتصال بمدير الإنترنت</value>
|
||||
</data>
|
||||
<data name="400_Error_Title" xml:space="preserve">
|
||||
<value>يبدو أن خطأ غير متوقع حدث.</value>
|
||||
</data>
|
||||
<data name="401_Error_Message" xml:space="preserve">
|
||||
<value>يبدو أنك غير مصرح لك بالنظر إلى تلك الصفحة.</value>
|
||||
</data>
|
||||
<data name="401_Error_Title" xml:space="preserve">
|
||||
<value>Access Denied</value>
|
||||
</data>
|
||||
<data name="402_Error_Message" xml:space="preserve">
|
||||
<value>المفتاح السري كان غير صحيح</value>
|
||||
</data>
|
||||
<data name="402_Error_Title" xml:space="preserve">
|
||||
<value>مفتاح سرّي غير مستقر</value>
|
||||
</data>
|
||||
<data name="403_Error_Message" xml:space="preserve">
|
||||
<value>المجوهرات المحددة لا وجود لها وليس لديك إذن لخلقها</value>
|
||||
</data>
|
||||
<data name="403_Error_Title" xml:space="preserve">
|
||||
<value>Access Denied</value>
|
||||
</data>
|
||||
<data name="405_Error_Message" xml:space="preserve">
|
||||
<value>لقد وصلت إلى الحد الأقصى لخلق المعرض</value>
|
||||
</data>
|
||||
<data name="405_Error_Title" xml:space="preserve">
|
||||
<value>Limit Reached</value>
|
||||
</data>
|
||||
<data name="406_Error_Message" xml:space="preserve">
|
||||
<value>المعرض المحدد لا يوجد</value>
|
||||
</data>
|
||||
<data name="406_Error_Title" xml:space="preserve">
|
||||
<value>جالري</value>
|
||||
</data>
|
||||
<data name="Actions" xml:space="preserve">
|
||||
<value>الإجراءات</value>
|
||||
</data>
|
||||
<data name="Add" xml:space="preserve">
|
||||
<value>مضافا إليها</value>
|
||||
</data>
|
||||
<data name="Approve" xml:space="preserve">
|
||||
<value>الموافقة</value>
|
||||
</data>
|
||||
<data name="Approved" xml:space="preserve">
|
||||
<value>الوظائف المعتمدة</value>
|
||||
</data>
|
||||
<data name="Available_Galleries" xml:space="preserve">
|
||||
<value>المتاح</value>
|
||||
</data>
|
||||
<data name="Clean" xml:space="preserve">
|
||||
<value>نظيفة</value>
|
||||
</data>
|
||||
<data name="Clean_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد تنظيف هذا المعرض؟?</value>
|
||||
</data>
|
||||
<data name="Click_To_Upload" xml:space="preserve">
|
||||
<value>انقر هنا لاختيار الملفات</value>
|
||||
</data>
|
||||
<data name="Close" xml:space="preserve">
|
||||
<value>اقترب</value>
|
||||
</data>
|
||||
<data name="Create" xml:space="preserve">
|
||||
<value>الإبداع</value>
|
||||
</data>
|
||||
<data name="Danger_Zone" xml:space="preserve">
|
||||
<value>منطقة الخطر</value>
|
||||
</data>
|
||||
<data name="Default" xml:space="preserve">
|
||||
<value>التقصير</value>
|
||||
</data>
|
||||
<data name="Delete" xml:space="preserve">
|
||||
<value>تحذف</value>
|
||||
</data>
|
||||
<data name="Delete_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد حذف هذا المعرض؟?</value>
|
||||
</data>
|
||||
<data name="Destructive_Action_Performed" xml:space="preserve">
|
||||
<value>الإجراءات المدمرة</value>
|
||||
</data>
|
||||
<data name="Download_Gallery_Not_Allowed" xml:space="preserve">
|
||||
<value>لا يسمح لك بتحميل هذا المعرض</value>
|
||||
</data>
|
||||
<data name="Drag_And_Drop" xml:space="preserve">
|
||||
<value>Drag</value>
|
||||
</data>
|
||||
<data name="Duplicate_Item_Detected" xml:space="preserve">
|
||||
<value>ملف مزدوج مكتشف</value>
|
||||
</data>
|
||||
<data name="Export" xml:space="preserve">
|
||||
<value>الصادرات</value>
|
||||
</data>
|
||||
<data name="Failed_Add_Gallery" xml:space="preserve">
|
||||
<value>فشل في خلق المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Add_User" xml:space="preserve">
|
||||
<value>فشل في خلق مستعمل</value>
|
||||
</data>
|
||||
<data name="Failed_Delete_Gallery" xml:space="preserve">
|
||||
<value>فشل في حذف المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Delete_User" xml:space="preserve">
|
||||
<value>عدم حذف المستخدم</value>
|
||||
</data>
|
||||
<data name="Failed_Download_Gallery" xml:space="preserve">
|
||||
<value>فشل في تنزيل المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Edit_Gallery" xml:space="preserve">
|
||||
<value>فشل في تحديث المعرض</value>
|
||||
</data>
|
||||
<data name="Failed_Edit_User" xml:space="preserve">
|
||||
<value>لم يستكمل المستعمل</value>
|
||||
</data>
|
||||
<data name="Failed_Export" xml:space="preserve">
|
||||
<value>الفشل في الحصول على بيانات التصدير</value>
|
||||
</data>
|
||||
<data name="Failed_Finding_File" xml:space="preserve">
|
||||
<value>فشل في العثور على ملف</value>
|
||||
</data>
|
||||
<data name="Failed_Import" xml:space="preserve">
|
||||
<value>الفشل في الحصول على بيانات الاستيراد</value>
|
||||
</data>
|
||||
<data name="Failed_Reviewing_Media" xml:space="preserve">
|
||||
<value>عدم استعراض البند</value>
|
||||
</data>
|
||||
<data name="Failed_Update_Setting" xml:space="preserve">
|
||||
<value>فشل في تحديث الأطر</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Galleries" xml:space="preserve">
|
||||
<value>فشل في مسح المغالي</value>
|
||||
</data>
|
||||
<data name="Failed_Wipe_Gallery" xml:space="preserve">
|
||||
<value>فشل في محو المعرض</value>
|
||||
</data>
|
||||
<data name="FFMPEG_Download_Failed" xml:space="preserve">
|
||||
<value>فشل في تحميل FFMPEG إلى الطريق</value>
|
||||
</data>
|
||||
<data name="FFMPEG_Downloading" xml:space="preserve">
|
||||
<value>محاولاً توليد إبهام بالفيديو، لكن شركة FMPEG لم تستكمل التحميل بعد. إذا كانت هذه تركيبة جديدة، يرجى الانتظار لتنزيلها لإكمال وضياع الإبهام</value>
|
||||
</data>
|
||||
<data name="File_Upload_Failed" xml:space="preserve">
|
||||
<value>فشل في رفع الملف</value>
|
||||
</data>
|
||||
<data name="Filter" xml:space="preserve">
|
||||
<value>فيلم</value>
|
||||
</data>
|
||||
<data name="Galleries" xml:space="preserve">
|
||||
<value>Galleries</value>
|
||||
</data>
|
||||
<data name="Gallery" xml:space="preserve">
|
||||
<value>Gallery</value>
|
||||
</data>
|
||||
<data name="Gallery_Does_Not_Exist" xml:space="preserve">
|
||||
<value>The requested gallery was not found or does not exist</value>
|
||||
</data>
|
||||
<data name="Gallery_Empty" xml:space="preserve">
|
||||
<value>لا توجد حاليا ذكريات مشتركة</value>
|
||||
</data>
|
||||
<data name="Gallery_Empty_Upload" xml:space="preserve">
|
||||
<value>لا توجد حالياً ذكريات مشتركة لماذا لا تحملين ذكرياتك!</value>
|
||||
</data>
|
||||
<data name="Gallery_Full" xml:space="preserve">
|
||||
<value>المعرض ممتلئ وقد تجاوز الحد الأقصى لحجم</value>
|
||||
</data>
|
||||
<data name="Gallery_Key" xml:space="preserve">
|
||||
<value>مفتاح المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Key_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل المفتاح السري.</value>
|
||||
</data>
|
||||
<data name="Gallery_Key_Placeholder" xml:space="preserve">
|
||||
<value>يوم الزفاف</value>
|
||||
</data>
|
||||
<data name="Gallery_Limit_Reached" xml:space="preserve">
|
||||
<value>تم الوصول إلى الحد الأقصى المسموح به من المغالي</value>
|
||||
</data>
|
||||
<data name="Gallery_List_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على قائمة المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Name" xml:space="preserve">
|
||||
<value>اسم المعرض</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Already_Exists" xml:space="preserve">
|
||||
<value>اسم المعرض موجود بالفعل</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Help" xml:space="preserve">
|
||||
<value>رجاءً أدخلي اسم المعرض الذي تريدين زيارته.</value>
|
||||
</data>
|
||||
<data name="Gallery_Name_Placeholder" xml:space="preserve">
|
||||
<value>حفل زفافي</value>
|
||||
</data>
|
||||
<data name="Gallery_Selector_Title" xml:space="preserve">
|
||||
<value>Gallery Selector</value>
|
||||
</data>
|
||||
<data name="Generate" xml:space="preserve">
|
||||
<value>الجيل</value>
|
||||
</data>
|
||||
<data name="Group" xml:space="preserve">
|
||||
<value>المجموعة</value>
|
||||
</data>
|
||||
<data name="Homepage_Load_Error" xml:space="preserve">
|
||||
<value>وقع خطأ في تحميل الصفحة الرئيسية</value>
|
||||
</data>
|
||||
<data name="Identity_Session_Error" xml:space="preserve">
|
||||
<value>عدم تحديد جلسة هوية المستخدمين - الاسم</value>
|
||||
</data>
|
||||
<data name="Image_Upload_Failed" xml:space="preserve">
|
||||
<value>فشل في تحميل الصور</value>
|
||||
</data>
|
||||
<data name="Import" xml:space="preserve">
|
||||
<value>الواردات</value>
|
||||
</data>
|
||||
<data name="Invalid_Access_Token" xml:space="preserve">
|
||||
<value>The provided access token was invalid</value>
|
||||
</data>
|
||||
<data name="Invalid_File_Type" xml:space="preserve">
|
||||
<value>نوع الملف غير صحيح</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Id" xml:space="preserve">
|
||||
<value>معرض غير رسمي Id</value>
|
||||
</data>
|
||||
<data name="Invalid_Gallery_Key" xml:space="preserve">
|
||||
<value>مفتاح المعرض الفاشل</value>
|
||||
</data>
|
||||
<data name="Invalid_Secret_Key_Warning" xml:space="preserve">
|
||||
<value>قُدم طلب باستخدام مفتاح سري غير صحيح</value>
|
||||
</data>
|
||||
<data name="Key" xml:space="preserve">
|
||||
<value>المفتاح</value>
|
||||
</data>
|
||||
<data name="Link" xml:space="preserve">
|
||||
<value>Link</value>
|
||||
</data>
|
||||
<data name="Login" xml:space="preserve">
|
||||
<value>Login</value>
|
||||
</data>
|
||||
<data name="Login_Failed" xml:space="preserve">
|
||||
<value>فشل في تسجيل المستخدمين</value>
|
||||
</data>
|
||||
<data name="Login_Password_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل كلمة السر.</value>
|
||||
</data>
|
||||
<data name="Login_Password_Placeholder" xml:space="preserve">
|
||||
<value>كلمة السر</value>
|
||||
</data>
|
||||
<data name="Login_Username_Help" xml:space="preserve">
|
||||
<value>من فضلك أدخل اسم المستخدم.</value>
|
||||
</data>
|
||||
<data name="Login_Username_Placeholder" xml:space="preserve">
|
||||
<value>Admin</value>
|
||||
</data>
|
||||
<data name="Logout" xml:space="preserve">
|
||||
<value>Logout</value>
|
||||
</data>
|
||||
<data name="Manage_Data" xml:space="preserve">
|
||||
<value>بيانات إدارة</value>
|
||||
</data>
|
||||
<data name="Max_File_Size" xml:space="preserve">
|
||||
<value>حجم ملف (ماكس)</value>
|
||||
</data>
|
||||
<data name="MultiFactor_Token_Set_Failed" xml:space="preserve">
|
||||
<value>فشل في إنشاء 2FA</value>
|
||||
</data>
|
||||
<data name="Name" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Name_Cannot_Be_Blank" xml:space="preserve">
|
||||
<value>اسم المعرض لا يمكن أن يكون فارغا</value>
|
||||
</data>
|
||||
<data name="New_Items_Pending_Review" xml:space="preserve">
|
||||
<value>البنود الجديدة قيد الاستعراض</value>
|
||||
</data>
|
||||
<data name="No_Data" xml:space="preserve">
|
||||
<value>لا توجد بيانات</value>
|
||||
</data>
|
||||
<data name="No_Files_For_Upload" xml:space="preserve">
|
||||
<value>No files were detected for upload</value>
|
||||
</data>
|
||||
<data name="No_Galleries" xml:space="preserve">
|
||||
<value>لا توجد مسابقات بعد</value>
|
||||
</data>
|
||||
<data name="No_Pending_Uploads" xml:space="preserve">
|
||||
<value>No uploads pending review</value>
|
||||
</data>
|
||||
<data name="Pending" xml:space="preserve">
|
||||
<value>معلقة</value>
|
||||
</data>
|
||||
<data name="Pending_Uploads" xml:space="preserve">
|
||||
<value>تحميل</value>
|
||||
</data>
|
||||
<data name="Pending_Uploads_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على تحميل</value>
|
||||
</data>
|
||||
<data name="Reject" xml:space="preserve">
|
||||
<value>Reject</value>
|
||||
</data>
|
||||
<data name="Rename" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Review" xml:space="preserve">
|
||||
<value>الاستعراض</value>
|
||||
</data>
|
||||
<data name="Save_QR" xml:space="preserve">
|
||||
<value>أنقذوا (كيو آر)</value>
|
||||
</data>
|
||||
<data name="Save_To_Gallery_Failed" xml:space="preserve">
|
||||
<value>فشل في توفير الصور للمعرض</value>
|
||||
</data>
|
||||
<data name="Scan_To_Share" xml:space="preserve">
|
||||
<value>قوموا بمسح شفرة (كيو آر) بهواتفكم لتشاركوا صوركم</value>
|
||||
</data>
|
||||
<data name="Settings" xml:space="preserve">
|
||||
<value>الترتيبات</value>
|
||||
</data>
|
||||
<data name="Settings_Account" xml:space="preserve">
|
||||
<value>الحساب</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Attempts_Help" xml:space="preserve">
|
||||
<value>كم عدد قطع الأشجار الفاشلة قبل أن يغلق المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Attempts_Label" xml:space="preserve">
|
||||
<value>أعمال الضبط</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Mins_Help" xml:space="preserve">
|
||||
<value>كم من الوقت يجب أن يغلق المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Lockout_Mins_Label" xml:space="preserve">
|
||||
<value>أجهزة المراقبة</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Profile_Icon_Help" xml:space="preserve">
|
||||
<value>إذا ظهرت الصورة في الرأس</value>
|
||||
</data>
|
||||
<data name="Settings_Account_Profile_Icon_Label" xml:space="preserve">
|
||||
<value>Profile Icon</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts" xml:space="preserve">
|
||||
<value>إنذار</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Account_Lockout_Help" xml:space="preserve">
|
||||
<value>يجب أن ترسل التحذيرات عندما تكون الحسابات مغلقة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Account_Lockout_Label" xml:space="preserve">
|
||||
<value>حساب القفل</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Destructive_Action_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات عند القيام بعمل تدميري</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Destructive_Action_Label" xml:space="preserve">
|
||||
<value>الأعمال المدمرة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Failed_Login_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات في محاولات قطع الأشجار الفاشلة</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Failed_Login_Label" xml:space="preserve">
|
||||
<value>Failed Login</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Pending_Review_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال الإنذارات إذا كان هناك بند جديد قيد الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Alerts_Pending_Review_Label" xml:space="preserve">
|
||||
<value>استعراض انتظار</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices" xml:space="preserve">
|
||||
<value>خدمات المعلومات الأساسية</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_Cleanup_Help" xml:space="preserve">
|
||||
<value>تنظيف الملفات المؤقتة</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_Cleanup_Label" xml:space="preserve">
|
||||
<value>التنظيف</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_DirectoryScanner_Help" xml:space="preserve">
|
||||
<value>التقطت الملفات مباشرة إلى ملف المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_DirectoryScanner_Label" xml:space="preserve">
|
||||
<value>Directory Scanner</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_EmailReport_Help" xml:space="preserve">
|
||||
<value>أرسل تقرير عن حالة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_BackgroundServices_EmailReport_Label" xml:space="preserve">
|
||||
<value>Email Reports</value>
|
||||
</data>
|
||||
<data name="Settings_Basic" xml:space="preserve">
|
||||
<value>أساسي</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_BaseUrl_Help" xml:space="preserve">
|
||||
<value>قاعدة أورل التي ستستخدم في الوصلات والإخطارات</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_BaseUrl_Label" xml:space="preserve">
|
||||
<value>القاعدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_EmailReport_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال تقرير بريد إلكتروني مع إحصائيات المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_EmailReport_Label" xml:space="preserve">
|
||||
<value>Email Report</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ForceHttps_Help" xml:space="preserve">
|
||||
<value>يجب أن Htp users be forced to https</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ForceHttps_Label" xml:space="preserve">
|
||||
<value>القوة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorDropdown_Help" xml:space="preserve">
|
||||
<value>كيف يختار المستعملون معرضاً</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorDropdown_Label" xml:space="preserve">
|
||||
<value>Gallery Selector النوع</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorHideDefault_Help" xml:space="preserve">
|
||||
<value>إذا كان خيار المعرض الافتراضي واضح</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GallerySelectorHideDefault_Label" xml:space="preserve">
|
||||
<value>معرض الدفن</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GuestGalleryCreation_Help" xml:space="preserve">
|
||||
<value>هل يُسمح للضيوف أن يخلقوا مجالات جديدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_GuestGalleryCreation_Label" xml:space="preserve">
|
||||
<value>مهرجان الضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HideKeyFromQRCode_Help" xml:space="preserve">
|
||||
<value>إذا أُدرج المفتاح السرّي في رمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HideKeyFromQRCode_Label" xml:space="preserve">
|
||||
<value>Insecure QR Codes</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HomeLink_Help" xml:space="preserve">
|
||||
<value>يَجِبُ أَنْ يَنْقرَ الرابطَ المنزليَ إعادة توجيه المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_HomeLink_Label" xml:space="preserve">
|
||||
<value>Clickable Home Link</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_LinksOpenNewTab_Help" xml:space="preserve">
|
||||
<value>يجب أن تُفتح وصلات جديدة</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_LinksOpenNewTab_Label" xml:space="preserve">
|
||||
<value>New Tab Links</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_MaxGalleryCount_Help" xml:space="preserve">
|
||||
<value>كم عدد المجرات المسموح للمستعملين بصنعها</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_MaxGalleryCount_Label" xml:space="preserve">
|
||||
<value>ماكس غاليري كونت</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_SingleGalleryMode_Help" xml:space="preserve">
|
||||
<value>يجب أن يكون هناك مهر واحد أو أكثر</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_SingleGalleryMode_Label" xml:space="preserve">
|
||||
<value>Gallery Mode</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ThumbnailSize_Help" xml:space="preserve">
|
||||
<value>الحد الأقصى للبكازات في أي اتجاه للإبهام</value>
|
||||
</data>
|
||||
<data name="Settings_Basic_ThumbnailSize_Label" xml:space="preserve">
|
||||
<value>حل الإبهام</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery" xml:space="preserve">
|
||||
<value>التخلف عن الدفع</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_AllowedFileTypes_Help" xml:space="preserve">
|
||||
<value>ما هو نوع الملف المسموح به للضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_AllowedFileTypes_Label" xml:space="preserve">
|
||||
<value>أنواع الملفات المسموح بها</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_BannerImage_Help" xml:space="preserve">
|
||||
<value>صورة لعرضها على قمة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_BannerImage_Label" xml:space="preserve">
|
||||
<value>طراز Banner Image</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Columns_Help" xml:space="preserve">
|
||||
<value>عدد الأعمدة المعروضة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Columns_Label" xml:space="preserve">
|
||||
<value>Column count</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_DefaultView_Help" xml:space="preserve">
|
||||
<value>ما هو مشهد قافلة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_DefaultView_Label" xml:space="preserve">
|
||||
<value>الصورة الافتراضية</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Download_Help" xml:space="preserve">
|
||||
<value>هل يسمح للضيوف بتحميل المواد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Download_Label" xml:space="preserve">
|
||||
<value>السماح بتحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_FullWidth_Help" xml:space="preserve">
|
||||
<value>عرض المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_FullWidth_Label" xml:space="preserve">
|
||||
<value>نوع العرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_IdleRefreshMins_Help" xml:space="preserve">
|
||||
<value>بعد كم من الوقت يجب أن ننعش المعرض للحصول على محتوى جديد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_IdleRefreshMins_Label" xml:space="preserve">
|
||||
<value>Idle Refresh Min</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ItemsPerPage_Help" xml:space="preserve">
|
||||
<value>العدد الأقصى للأصناف المراد عرضها في الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ItemsPerPage_Label" xml:space="preserve">
|
||||
<value>البنود الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxFileSizeMb_Help" xml:space="preserve">
|
||||
<value>ما هو أكبر في MB ملف محمول يمكن أن يكون</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxFileSizeMb_Label" xml:space="preserve">
|
||||
<value>Max File Size (MB)</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxSizeMb_Help" xml:space="preserve">
|
||||
<value>كم هو كبير في إم بي هو المعرض المسموح له أن ينمو</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_MaxSizeMb_Label" xml:space="preserve">
|
||||
<value>مقاس ماكس غالاري</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_PreventDuplicates_Help" xml:space="preserve">
|
||||
<value>في حالة رفض ازدواجية البنود تلقائياً</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_PreventDuplicates_Label" xml:space="preserve">
|
||||
<value>منع الازدواجية</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultSort_Help" xml:space="preserve">
|
||||
<value>مرة واحدة مسح ما هو النظام</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultSort_Label" xml:space="preserve">
|
||||
<value>Default QR Sort</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultView_Help" xml:space="preserve">
|
||||
<value>مرة واحدة مسح ما التصميم إذا رأى المستخدم</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeDefaultView_Label" xml:space="preserve">
|
||||
<value>Default QR View</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeEnabled_Help" xml:space="preserve">
|
||||
<value>إذا تم عرض رمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_QRCodeEnabled_Label" xml:space="preserve">
|
||||
<value>يُظهرُ الرمز QR</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Quote_Help" xml:space="preserve">
|
||||
<value>اقتباس للعرض على قمة المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Quote_Label" xml:space="preserve">
|
||||
<value>Quote</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RequireReview_Help" xml:space="preserve">
|
||||
<value>هل يتعين استعراض البنود قبل أن تظهر في الموقع</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RequireReview_Label" xml:space="preserve">
|
||||
<value>الاستعراض المطلوب</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RetainRejectedItems_Help" xml:space="preserve">
|
||||
<value>ينبغي تخزين الأصناف المرفوضة لاستخدامها في وقت لاحق</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_RetainRejectedItems_Label" xml:space="preserve">
|
||||
<value>Retain Rejected items</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ReviewCounter_Help" xml:space="preserve">
|
||||
<value>في حالة عرض مكتب الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_ReviewCounter_Label" xml:space="preserve">
|
||||
<value>الاستعراض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_SecretKey_Help" xml:space="preserve">
|
||||
<value>كلمة سر لفتح المعرض</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_SecretKey_Label" xml:space="preserve">
|
||||
<value>المفتاح السري</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Upload_Help" xml:space="preserve">
|
||||
<value>هل يسمح للضيوف بتحميل المواد</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_Upload_Label" xml:space="preserve">
|
||||
<value>السماح بتحميلات</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_UploadPeriod_Help" xml:space="preserve">
|
||||
<value>تقييد الحمولات لفترة محددة</value>
|
||||
</data>
|
||||
<data name="Settings_Gallery_UploadPeriod_Label" xml:space="preserve">
|
||||
<value>فترات تحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify" xml:space="preserve">
|
||||
<value>التحصيل</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Enabled_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال إنذارات نتيفي</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Endpoint_Help" xml:space="preserve">
|
||||
<value>عنوان الخادم</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Endpoint_Label" xml:space="preserve">
|
||||
<value>نقطة النهاية</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Priority_Help" xml:space="preserve">
|
||||
<value>قيمة الإنذارات الهامة</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Priority_Label" xml:space="preserve">
|
||||
<value>الأولوية</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Token_Help" xml:space="preserve">
|
||||
<value>المركب المستخدم للتوثيق</value>
|
||||
</data>
|
||||
<data name="Settings_Gotify_Token_Label" xml:space="preserve">
|
||||
<value>Access Token</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck" xml:space="preserve">
|
||||
<value>التحقق من الهوية</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Enabled_Help" xml:space="preserve">
|
||||
<value>المستعملون الفوريون لإسمهم</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_PageLoad_Help" xml:space="preserve">
|
||||
<value>إذا كان فحص الهوية يُعرض في الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_PageLoad_Label" xml:space="preserve">
|
||||
<value>الصفحة</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Upload_Help" xml:space="preserve">
|
||||
<value>إذا طُلب التحقق من الهوية لتحميل وسائط الإعلام</value>
|
||||
</data>
|
||||
<data name="Settings_IdentityCheck_Upload_Label" xml:space="preserve">
|
||||
<value>تحميل</value>
|
||||
</data>
|
||||
<data name="Settings_Languages" xml:space="preserve">
|
||||
<value>اللغات</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Default_Help" xml:space="preserve">
|
||||
<value>:: لغة الموقع الشبكي الافتراضي إذا لم يُختار أحد</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Default_Label" xml:space="preserve">
|
||||
<value>اللغة الافتراضية</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Enabled_Help" xml:space="preserve">
|
||||
<value>السماح للمستعملين بتغيير اللغة</value>
|
||||
</data>
|
||||
<data name="Settings_Languages_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy" xml:space="preserve">
|
||||
<value>Ntfy</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Enabled_Help" xml:space="preserve">
|
||||
<value>هل ينبغي إرسال إنذارات نتيفي</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Endpoint_Help" xml:space="preserve">
|
||||
<value>عنوان الخادم</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Endpoint_Label" xml:space="preserve">
|
||||
<value>نقطة النهاية</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Priority_Help" xml:space="preserve">
|
||||
<value>قيمة الإنذارات الهامة</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Priority_Label" xml:space="preserve">
|
||||
<value>الأولوية</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Token_Help" xml:space="preserve">
|
||||
<value>المركب المستخدم للتوثيق</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Token_Label" xml:space="preserve">
|
||||
<value>Access Token</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Topic_Help" xml:space="preserve">
|
||||
<value>موضوع إرسال إنذارات</value>
|
||||
</data>
|
||||
<data name="Settings_Ntfy_Topic_Label" xml:space="preserve">
|
||||
<value>الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow" xml:space="preserve">
|
||||
<value>Slideshow</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Fade_Help" xml:space="preserve">
|
||||
<value>كم من الوقت يجب أن يكون الانتقال بين الشرائح</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Fade_Label" xml:space="preserve">
|
||||
<value>Fade</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Interval_Help" xml:space="preserve">
|
||||
<value>كم من الوقت في الثواني يجب أن ينزلق قبل الانتقال إلى الشريحة التالية</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Interval_Label" xml:space="preserve">
|
||||
<value>الفترات الفاصلة</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Limit_Help" xml:space="preserve">
|
||||
<value>الحد الأقصى لعدد الشرائح التي ستدرج</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Limit_Label" xml:space="preserve">
|
||||
<value>Limit</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Share_Slide_Help" xml:space="preserve">
|
||||
<value>هل يجب أن تُرفض شفرة (كيو آر) في النهاية للضيوف</value>
|
||||
</data>
|
||||
<data name="Settings_Slideshow_Share_Slide_Label" xml:space="preserve">
|
||||
<value>Include Share Slide</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp" xml:space="preserve">
|
||||
<value>Email</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Display_Name_Help" xml:space="preserve">
|
||||
<value>اسم المرسل الصديق للمستعمل</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Display_Name_Label" xml:space="preserve">
|
||||
<value>الاسم</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Enabled_Help" xml:space="preserve">
|
||||
<value>ينبغي إرسال تنبيهات بالبريد الإلكتروني</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_From_Help" xml:space="preserve">
|
||||
<value>عنوان البريد الإلكتروني أرسل من</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_From_Label" xml:space="preserve">
|
||||
<value>من</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Host_Help" xml:space="preserve">
|
||||
<value>The hostname of the SMTP provider</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Host_Label" xml:space="preserve">
|
||||
<value>المضيفة</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Password_Help" xml:space="preserve">
|
||||
<value>كلمة السر المستخدمة لتوثيق الطلب</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Password_Label" xml:space="preserve">
|
||||
<value>كلمة السر</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Port_Help" xml:space="preserve">
|
||||
<value>The port used by the SMTP provider</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Port_Label" xml:space="preserve">
|
||||
<value>الميناء</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Recipient_Help" xml:space="preserve">
|
||||
<value>قائمة بريد إلكتروني منفصلة</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Recipient_Label" xml:space="preserve">
|
||||
<value>المتلقين</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Use_SSL_Help" xml:space="preserve">
|
||||
<value>هل يستخدم مقدم خدمات SMTP SSL</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Use_SSL_Label" xml:space="preserve">
|
||||
<value>Use SSL</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Username_Help" xml:space="preserve">
|
||||
<value>الاسم المستخدم لتوثيق الطلب</value>
|
||||
</data>
|
||||
<data name="Settings_Smtp_Username_Label" xml:space="preserve">
|
||||
<value>المستعمل</value>
|
||||
</data>
|
||||
<data name="Settings_Themes" xml:space="preserve">
|
||||
<value>المواضيع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Default_Help" xml:space="preserve">
|
||||
<value>الموضوع الرئيسي للموقع إذا لم يُختار أحد</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Default_Label" xml:space="preserve">
|
||||
<value>التقصير الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Enabled_Help" xml:space="preserve">
|
||||
<value>السماح للمستعملين بتغيير الموضوع</value>
|
||||
</data>
|
||||
<data name="Settings_Themes_Enabled_Label" xml:space="preserve">
|
||||
<value>Enabled</value>
|
||||
</data>
|
||||
<data name="Share_Code" xml:space="preserve">
|
||||
<value>Share Code</value>
|
||||
</data>
|
||||
<data name="Sort" xml:space="preserve">
|
||||
<value>Sort</value>
|
||||
</data>
|
||||
<data name="Space_Percent_Used" xml:space="preserve">
|
||||
<value>Usage</value>
|
||||
</data>
|
||||
<data name="Total" xml:space="preserve">
|
||||
<value>المجموع</value>
|
||||
</data>
|
||||
<data name="Unknown_Review_Action" xml:space="preserve">
|
||||
<value>إجراء استعراض غير معروف</value>
|
||||
</data>
|
||||
<data name="Update" xml:space="preserve">
|
||||
<value>آخر</value>
|
||||
</data>
|
||||
<data name="Upload_Media" xml:space="preserve">
|
||||
<value>تحميل وسائط الإعلام</value>
|
||||
</data>
|
||||
<data name="Uploaded_By" xml:space="preserve">
|
||||
<value>محمولة</value>
|
||||
</data>
|
||||
<data name="Uploader" xml:space="preserve">
|
||||
<value>الحمولة</value>
|
||||
</data>
|
||||
<data name="User_CPassword_Missmatch" xml:space="preserve">
|
||||
<value>كلمة السر المقدّمة وكلمة سر الكونفريم لا تتطابق</value>
|
||||
</data>
|
||||
<data name="User_Name_Already_Exists" xml:space="preserve">
|
||||
<value>يضاف المستخدم الذي يحمل نفس الاسم موجود بالفعل</value>
|
||||
</data>
|
||||
<data name="Username" xml:space="preserve">
|
||||
<value>المستعمل</value>
|
||||
</data>
|
||||
<data name="Users" xml:space="preserve">
|
||||
<value>المستخدمون</value>
|
||||
</data>
|
||||
<data name="Users_List_Failed" xml:space="preserve">
|
||||
<value>فشل في الحصول على قائمة المستخدمين</value>
|
||||
</data>
|
||||
<data name="View" xml:space="preserve">
|
||||
<value>View</value>
|
||||
</data>
|
||||
<data name="Visit" xml:space="preserve">
|
||||
<value>زيارة</value>
|
||||
</data>
|
||||
<data name="Wipe" xml:space="preserve">
|
||||
<value>Wipe</value>
|
||||
</data>
|
||||
<data name="Wipe_All_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد من أنك تريد مسح كل المجرات؟?</value>
|
||||
</data>
|
||||
<data name="Wipe_Confirmation" xml:space="preserve">
|
||||
<value>هل أنت متأكد أنك تريد مسح هذا المعرض؟?</value>
|
||||
</data>
|
||||
</root>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user