Compare commits
64 Commits
d0ec91aa2a
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01a9fd654d | ||
|
|
e296b142a3 | ||
|
|
4cab2988e1 | ||
|
|
0a57919271 | ||
|
|
1fd6ebc570 | ||
|
|
5827f88956 | ||
|
|
ca290bcba7 | ||
|
|
fe544ebf24 | ||
|
|
21d99ef14f | ||
|
|
cec2d805ca | ||
|
|
0b9acbf7b3 | ||
|
|
0a814ec6c0 | ||
|
|
597533d70c | ||
|
|
8ead1d0ee1 | ||
|
|
1db0ea89cf | ||
|
|
c6a61879d9 | ||
|
|
2e882e7c13 | ||
|
|
6a47f84fcc | ||
|
|
b0e84119ac | ||
|
|
597b20483c | ||
|
|
e411c0ce27 | ||
|
|
8f18978ad9 | ||
|
|
d99cc61056 | ||
|
|
0ae0f4ec35 | ||
|
|
5a05822a9f | ||
|
|
445ae26360 | ||
|
|
adad2dcfd9 | ||
|
|
7c58a270b3 | ||
|
|
83ecd83f83 | ||
|
|
c07332657b | ||
|
|
860ed07429 | ||
|
|
1d7bb08b79 | ||
|
|
bcd85b8f61 | ||
|
|
d52f4da3bb | ||
|
|
82c6394c82 | ||
|
|
330071a864 | ||
|
|
040e590802 | ||
|
|
c062b4073c | ||
|
|
f1f3abb05a | ||
|
|
3ade5c8c64 | ||
|
|
7b472f337f | ||
|
|
3630291ad9 | ||
|
|
8265c7c051 | ||
|
|
f4773ec096 | ||
|
|
c00f9f505e | ||
|
|
5780aaec73 | ||
|
|
781c594f37 | ||
|
|
c31d698bdf | ||
|
|
c3c2a5810f | ||
|
|
6c1bd7d45f | ||
|
|
e95619c969 | ||
|
|
c52ddb9820 | ||
|
|
6994dbab7a | ||
|
|
21299543ec | ||
|
|
b80f6eb0a3 | ||
|
|
2033877fe9 | ||
|
|
2018fed2c9 | ||
|
|
fc086e63de | ||
|
|
278eaed931 | ||
|
|
020d8618aa | ||
|
|
89c0933f45 | ||
|
|
b0318c4d8b | ||
|
|
66fcba5d26 | ||
|
|
494531be62 |
12
.idea/.idea.MilkyShots/.idea/dataSources.xml
generated
12
.idea/.idea.MilkyShots/.idea/dataSources.xml
generated
@@ -27,5 +27,17 @@
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="TestDb@pm" uuid="a0f3e480-c856-41e0-8fb4-5cac87b9faf6">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://192.168.51.101:3306/TestDb</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="com.intellij.clouds.kubernetes.db.host.port" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
|
||||
<property name="com.intellij.clouds.kubernetes.db.container.port" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/.idea.MilkyShots/.idea/data_source_mapping.xml
generated
Normal file
6
.idea/.idea.MilkyShots/.idea/data_source_mapping.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourcePerFileMappings">
|
||||
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/80498c22-67da-4c4a-8106-c4789470ef83/console.sql" value="80498c22-67da-4c4a-8106-c4789470ef83" />
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/.idea.MilkyShots/.idea/jsLibraryMappings.xml
generated
Normal file
6
.idea/.idea.MilkyShots/.idea/jsLibraryMappings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="PROJECT" libraries="{bootstrap-icons}" />
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/.idea.MilkyShots/.idea/watcherTasks.xml
generated
2
.idea/.idea.MilkyShots/.idea/watcherTasks.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectTasksOptions">
|
||||
<TaskOptions isEnabled="false">
|
||||
<TaskOptions isEnabled="true">
|
||||
<option name="arguments" value="$FileName$:$FileNameWithoutExtension$.css" />
|
||||
<option name="checkSyntaxErrors" value="true" />
|
||||
<option name="description" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker">
|
||||
<configuration default="false" name="Compose Deployment" type="docker-deploy" factoryName="docker-compose.yml" server-name="Docker-PM">
|
||||
<deployment type="docker-compose.yml">
|
||||
<settings>
|
||||
<option name="containerName" value="" />
|
||||
@@ -18,16 +18,9 @@
|
||||
</list>
|
||||
</option>
|
||||
<option name="removeOrphansOnComposeDown" value="false" />
|
||||
<option name="services">
|
||||
<list>
|
||||
<option value="lactose" />
|
||||
<option value="database" />
|
||||
<option value="milkstream" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="commandLineOptions" value="--build" />
|
||||
<option name="sourceFilePath" value="docker-compose.yml" />
|
||||
<option name="upRemoveOrphans" value="true" />
|
||||
<option name="upRenewAnonVolumes" value="true" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<settings>
|
||||
<option name="imageTag" value="lactose" />
|
||||
<option name="containerName" value="lactose" />
|
||||
<option name="contextFolderPath" value="." />
|
||||
<option name="envVars">
|
||||
<list>
|
||||
<DockerEnvVarImpl>
|
||||
@@ -25,6 +26,7 @@
|
||||
</DockerPortBindingImpl>
|
||||
</list>
|
||||
</option>
|
||||
<option name="showCommandPreview" value="true" />
|
||||
<option name="sourceFilePath" value="Lactose/Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MilkStream HotReload" type="RunDotNetWatch" factoryName="RunDotNetWatch">
|
||||
<option name="envs">
|
||||
<map>
|
||||
<entry key="ASPNETCORE_ENVIRONMENT" value="Development" />
|
||||
<entry key="DOTNET_ENVIRONMENT" value="Hotreload" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="exePath" value="$PROJECT_DIR$/MilkStream/bin/Debug/net8.0/MilkStream.exe" />
|
||||
<option name="programParameters" value="" />
|
||||
<option name="projectFilePath" value="$PROJECT_DIR$/MilkStream/MilkStream.csproj" />
|
||||
<option name="projectTfm" value="net8.0" />
|
||||
<option name="suppressBrowserLaunch" value="true" />
|
||||
<option name="watchParameters" value="" />
|
||||
<option name="workingDirectory" value="$PROJECT_DIR$/MilkStream" />
|
||||
<method v="2" />
|
||||
|
||||
13
.run/MilkStream.run.xml
Normal file
13
.run/MilkStream.run.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="MilkStream" type="docker-deploy" factoryName="dockerfile" server-name="Docker-PM">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="containerName" value="milkstream" />
|
||||
<option name="contextFolderPath" value="." />
|
||||
<option name="sourceFilePath" value="MilkStream/Dockerfile" />
|
||||
</settings>
|
||||
</deployment>
|
||||
<EXTENSION ID="com.jetbrains.rider.docker.debug" isFastModeEnabled="true" isSslEnabled="false" />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Butter.Dtos;
|
||||
|
||||
public class RefreshDto {
|
||||
public required Guid UserId { get; set; }
|
||||
public required string RefreshToken { get; set; }
|
||||
public required Guid UserId { get; init; }
|
||||
public required string RefreshToken { get; init; }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using Butter.Settings;
|
||||
|
||||
namespace Butter.Dtos.Settings;
|
||||
|
||||
public class SettingDto {
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public required string Name { get; set; } = string.Empty;
|
||||
public string? Value { get; set; } = string.Empty;
|
||||
public required string? Description { get; set; }
|
||||
public required EType Type { get; set; }
|
||||
public required DisplayType DisplayType { get; set; }
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace Butter.Dtos.Settings;
|
||||
|
||||
public class SettingsDto {
|
||||
public required IEnumerable<SettingDto> Settings;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
using Butter.Types;
|
||||
|
||||
namespace Butter.Dtos.User;
|
||||
|
||||
public class UserInfoDto {
|
||||
@@ -10,4 +12,5 @@ public class UserInfoDto {
|
||||
public DateTime? BannedAt { get; set; }
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
public bool IsBanned { get; set; }
|
||||
public required EAccessLevel AccessLevel { get; set; } = EAccessLevel.User;
|
||||
}
|
||||
|
||||
104
Butter/MimeTypes.cs
Normal file
104
Butter/MimeTypes.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
namespace Butter;
|
||||
|
||||
public struct MimeTypeMap(string mimeType, string[] extensions) {
|
||||
public string MimeType { get; set; } = mimeType;
|
||||
public string[] Extensions { get; set; } = extensions;
|
||||
}
|
||||
|
||||
public static class MimeTypes{
|
||||
// ReSharper disable ArrangeObjectCreationWhenTypeEvident
|
||||
public static readonly MimeTypeMap[] Image = [
|
||||
new("image/avif", [".avif", ".avifs"]),
|
||||
new("image/bmp}", [".bmp"]),
|
||||
new("image/cgm", [".cgm"]),
|
||||
new("image/g3fax", [".g3"]),
|
||||
new("image/gif", [".gif"]),
|
||||
new("image/heic", [".heif", ".heic"]),
|
||||
new("image/ief", [".ief"]),
|
||||
new("image/jpeg", [".jpe", ".jpeg", ".jpg", ".pjpg", ".jfif", ".jfif-tbnl", ".jif"]),
|
||||
new("image/pjpeg", [".jpe", ".jpeg", ".jpg", ".pjpg", ".jfi", ".jfif", ".jfif-tbnl", ".jif"]),
|
||||
new("image/png", [".png"]),
|
||||
new("image/prs.btif", [".btif"]),
|
||||
new("image/svg+xml", [".svg", ".svgz"]),
|
||||
new("image/tiff", [".tif", ".tiff"]),
|
||||
new("image/vnd.adobe.photoshop", [".psd"]),
|
||||
new("image/vnd.djvu", [".djv", ".djvu"]),
|
||||
new("image/vnd.dwg", [".dwg"]),
|
||||
new("image/vnd.dxf", [".dxf"]),
|
||||
new("image/vnd.fastbidsheet", [".fbs"]),
|
||||
new("image/vnd.fpx", [".fpx"]),
|
||||
new("image/vnd.fst", [".fst"]),
|
||||
new("image/vnd.fujixerox.edmics-mmr", [".mmr"]),
|
||||
new("image/vnd.fujixerox.edmics-rlc", [".rlc"]),
|
||||
new("image/vnd.ms-modi", [".mdi"]),
|
||||
new("image/vnd.net-fpx", [".npx"]),
|
||||
new("image/vnd.wap.wbmp", [".wbmp"]),
|
||||
new("image/vnd.xiff", [".xif"]),
|
||||
new("image/webp", [".webp"]),
|
||||
new("image/x-adobe-dng", [".dng"]),
|
||||
new("image/x-canon-cr2", [".cr2"]),
|
||||
new("image/x-canon-crw", [".crw"]),
|
||||
new("image/x-cmu-raster", [".ras"]),
|
||||
new("image/x-cmx", [".cmx"]),
|
||||
new("image/x-epson-erf", [".erf"]),
|
||||
new("image/x-freehand", [".fh", ".fh4", ".fh5", ".fh7", ".fhc"]),
|
||||
new("image/x-fuji-raf", [".raf"]),
|
||||
new("image/x-icns", [".icns"]),
|
||||
new("image/x-icon", [".ico"]),
|
||||
new("image/x-kodak-dcr", [".dcr"]),
|
||||
new("image/x-kodak-k25", [".k25"]),
|
||||
new("image/x-kodak-kdc", [".kdc"]),
|
||||
new("image/x-minolta-mrw", [".mrw"]),
|
||||
new("image/x-nikon-nef", [".nef"]),
|
||||
new("image/x-olympus-orf", [".orf"]),
|
||||
new("image/x-panasonic-raw", [".raw", ".rw2", ".rwl"]),
|
||||
new("image/x-pcx", [".pcx"]),
|
||||
new("image/x-pentax-pef", [".pef", ".ptx"]),
|
||||
new("image/x-pict", [".pct", ".pic"]),
|
||||
new("image/x-portable-anymap", [".pnm"]),
|
||||
new("image/x-portable-bitmap", [".pbm"]),
|
||||
new("image/x-portable-graymap", [".pgm"]),
|
||||
new("image/x-portable-pixmap", [".ppm"]),
|
||||
new("image/x-rgb", [".rgb"]),
|
||||
new("image/x-sigma-x3f", [".x3f"]),
|
||||
new("image/x-sony-arw", [".arw"]),
|
||||
new("image/x-sony-sr2", [".sr2"]),
|
||||
new("image/x-sony-srf", [".srf"]),
|
||||
new("image/x-xbitmap", [".xbm"]),
|
||||
new("image/x-xpixmap", [".xpm"]),
|
||||
new("image/x-xwindowdump", [".xwd"])
|
||||
];
|
||||
|
||||
public static readonly MimeTypeMap[] Video = [
|
||||
new("video/3gpp", [".3gp"]),
|
||||
new("video/3gpp2", [".3g2"]),
|
||||
new("video/h261", [".h261"]),
|
||||
new("video/h263", [".h263"]),
|
||||
new("video/h264", [".h264"]),
|
||||
new("video/jpeg", [".jpgv"]),
|
||||
new("video/jpm", [".jpgm, .jpm"]),
|
||||
new("video/mj2", [".mj2, .mjp2"]),
|
||||
new("video/mp2t", [".ts"]),
|
||||
new("video/mp4", [".mp4, .mp4v, .mpg4"]),
|
||||
new("video/mpeg", [".m1v, .m2v, .mpa, .mpe, .mpeg, .mpg"]),
|
||||
new("video/ogg", [".ogv"]),
|
||||
new("video/quicktime", [".mov", ".qt"]),
|
||||
new("video/vnd.fvt", [".fvt"]),
|
||||
new("video/vnd.mpegurl", [".m4u", ".mxu"]),
|
||||
new("video/vnd.ms-playready.media.pyv", [".pyv"]),
|
||||
new("video/vnd.vivo", [".viv"]),
|
||||
new("video/webm", [".webm"]),
|
||||
new("video/x-f4v", [".f4v"]),
|
||||
new("video/x-fli", [".fli"]),
|
||||
new("video/x-flv", [".flv"]),
|
||||
new("video/x-m4v", [".m4v"]),
|
||||
new("video/x-matroska", [".mkv"]),
|
||||
new("video/x-ms-asf", [".asf", ".asx"]),
|
||||
new("video/x-ms-wm", [".wm"]),
|
||||
new("video/x-ms-wmv", [".wmv"]),
|
||||
new("video/x-ms-wmx", [".wmx"]),
|
||||
new("video/x-ms-wvx", [".wvx"]),
|
||||
new("video/x-msvideo", [".avi"]),
|
||||
new("video/x-sgi-movie", [".movie"])
|
||||
];
|
||||
}
|
||||
21
Butter/Settings/DisplayType.cs
Normal file
21
Butter/Settings/DisplayType.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace Butter.Settings;
|
||||
|
||||
public enum DisplayType {
|
||||
Text = 0,
|
||||
Password = 1,
|
||||
IntNumber = 2,
|
||||
FloatNumber = 3,
|
||||
Checkbox = 4,
|
||||
Switch = 5,
|
||||
DateTimePicker = 6,
|
||||
TimePicker = 7
|
||||
}
|
||||
|
||||
public static class DisplayTypeExtension {
|
||||
public static DisplayType? ToDisplayType(this string type) {
|
||||
var success = Enum.TryParse(type, out DisplayType result);
|
||||
if (success) return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
18
Butter/Settings/EType.cs
Normal file
18
Butter/Settings/EType.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace Butter.Settings;
|
||||
|
||||
public enum EType {
|
||||
String = 0,
|
||||
Integer = 1,
|
||||
Boolean = 2,
|
||||
Float = 3,
|
||||
DateTime = 4
|
||||
};
|
||||
|
||||
public static class ETypeExtension {
|
||||
public static EType? ToType(this string type) {
|
||||
var success = Enum.TryParse(type, out EType result);
|
||||
if (success) return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
22
Butter/Settings/Settings.cs
Normal file
22
Butter/Settings/Settings.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
namespace Butter.Settings;
|
||||
|
||||
public enum Settings {
|
||||
UserRegistrationEnabled, //Enable or disable self user registration
|
||||
FolderScanEnabled, // Used to enable/disable folder scanning
|
||||
FolderScanInterval, //Interval in minutes for folder scanning
|
||||
FileUploadEnabled, //Enable or disable file upload
|
||||
FileUploadMaxSize, //Maximum file size for uploads in bytes
|
||||
}
|
||||
|
||||
public static class SettingsExtensions {
|
||||
public static string AsString(this Settings setting) {
|
||||
return setting switch {
|
||||
Settings.UserRegistrationEnabled => "User Registration Enabled",
|
||||
Settings.FolderScanEnabled => "Folder Scan Enabled",
|
||||
Settings.FolderScanInterval => "Folder Scan Interval",
|
||||
Settings.FileUploadEnabled => "File Upload Enabled",
|
||||
Settings.FileUploadMaxSize => "File Upload MaxSize",
|
||||
_ => setting.ToString() // Fallback to the enum name
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -185,8 +185,10 @@ public class AuthController(
|
||||
Token = authService.GenerateAccessToken(user)
|
||||
};
|
||||
|
||||
// TODO: Decide if we want to change or extend refresh token
|
||||
if (user.RefreshTokenExpires > DateTime.Now - TimeSpan.FromMinutes(LactoseAuthService.BaseExpirationTime)) {
|
||||
if ((user.RefreshTokenExpires - TimeSpan.FromMinutes(LactoseAuthService.BaseExpirationTime)) < DateTime.Now) {
|
||||
var refreshToken = authService.GenerateRefreshToken(user);
|
||||
authDto.RefreshToken = refreshToken;
|
||||
user.RefreshToken = refreshToken;
|
||||
user.RefreshTokenExpires = DateTime.Now.AddMinutes(LactoseAuthService.LongExpirationTime);
|
||||
userRepository.Save();
|
||||
}
|
||||
|
||||
@@ -22,12 +22,19 @@ public class SettingsController(
|
||||
[Authorize(Roles = "Admin")]
|
||||
public ActionResult Create([FromBody] SettingDto settingDto) {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
|
||||
if (accessLevel != EAccessLevel.Admin) { return Unauthorized(); }
|
||||
|
||||
settingsRepository.Create(new Setting {
|
||||
settingsRepository.Create(
|
||||
new Setting {
|
||||
Name = settingDto.Name,
|
||||
Value = settingDto.Value
|
||||
});
|
||||
Value = settingDto.Value,
|
||||
Description = settingDto.Description,
|
||||
Type = settingDto.Type,
|
||||
DisplayType = settingDto.DisplayType
|
||||
}
|
||||
);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
@@ -37,17 +44,7 @@ public class SettingsController(
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) return Unauthorized();
|
||||
|
||||
return Ok(settingsRepository.Get().ToSettingsDto());
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Roles = "Admin")]
|
||||
[Route("get-all")]
|
||||
public ActionResult<SettingsDto> GetAll() {
|
||||
var accessLevel = authService.GetUserData(User)?.AccessLevel ?? EAccessLevel.User;
|
||||
if (accessLevel != EAccessLevel.Admin) return Unauthorized();
|
||||
|
||||
return Ok(settingsRepository.Get().ToSettingsDto());
|
||||
return Ok(settingsRepository.Get().Select(s => s.ToSettingDto()).ToList());
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Lactose.Controllers {
|
||||
LactoseAuthenticatedUser? authenticatedUser = authService.GetUserData(User);
|
||||
|
||||
if (authenticatedUser == null) {
|
||||
logger.LogWarning(@"Anonymous User hasn't enough permissions to create a User. it shouldn't be here");
|
||||
logger.LogWarning(@"Anonymous User hasn't enough permissions to read User data. it shouldn't be here");
|
||||
return Unauthorized("Unauthorized");
|
||||
}
|
||||
|
||||
|
||||
37
Lactose/DefaultSettings.json
Normal file
37
Lactose/DefaultSettings.json
Normal file
@@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"Name": "User Registration Enabled",
|
||||
"Value" : "false",
|
||||
"Description": "Sets if user registration is enabled or not.",
|
||||
"Type" : 2,
|
||||
"DisplayType" : 5
|
||||
},
|
||||
{
|
||||
"Name": "Folder Scan Enabled",
|
||||
"Value" : "true",
|
||||
"Description": "Sets if the folder scan service should be running or not.",
|
||||
"Type" : 2,
|
||||
"DisplayType" : 5
|
||||
},
|
||||
{
|
||||
"Name": "Folder Scan Interval",
|
||||
"Value" : "",
|
||||
"Description": "Time interval after the previous scan has finished before a new folder scan is started.",
|
||||
"Type" : 4,
|
||||
"DisplayType" : 7
|
||||
},
|
||||
{
|
||||
"Name": "File Upload Enabled",
|
||||
"Value" : "false",
|
||||
"Description": "NOT IMPLEMENTED YET: Sets if the file upload service should be running or not.",
|
||||
"Type" : 2,
|
||||
"DisplayType" : 5
|
||||
},
|
||||
{
|
||||
"Name": "File Upload MaxSize",
|
||||
"Value" : "0",
|
||||
"Description": "Max file size for uploads in bytes.",
|
||||
"Type" : 1,
|
||||
"DisplayType" :2
|
||||
}
|
||||
]
|
||||
@@ -7,13 +7,11 @@ EXPOSE 5162
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
|
||||
WORKDIR "/src"
|
||||
COPY ["./Lactose.csproj", "Lactose/"]
|
||||
COPY ["Lactose/Lactose.csproj", "Lactose/"]
|
||||
RUN dotnet restore "Lactose/Lactose.csproj"
|
||||
|
||||
WORKDIR "/src/Lactose"
|
||||
COPY . .
|
||||
WORKDIR "/src/Lactose"
|
||||
RUN dotnet build "Lactose.csproj" -c $BUILD_CONFIGURATION -o /app/build
|
||||
|
||||
FROM build AS publish
|
||||
|
||||
@@ -39,4 +39,10 @@
|
||||
<ProjectReference Include="..\Butter\Butter.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="DefaultSettings.json">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -14,7 +14,10 @@ public static class SettingsMapper {
|
||||
/// <returns>A SettingsDto object.</returns>
|
||||
public static SettingDto ToSettingDto(this Setting setting) => new SettingDto {
|
||||
Name = setting.Name,
|
||||
Value = setting.Value
|
||||
Value = setting.Value,
|
||||
Description = setting.Description,
|
||||
Type = setting.Type,
|
||||
DisplayType = setting.DisplayType
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
@@ -23,10 +26,9 @@ public static class SettingsMapper {
|
||||
/// <returns>A Settings object.</returns>
|
||||
public static Setting ToSetting(this SettingDto settingDto) => new Setting {
|
||||
Name = settingDto.Name,
|
||||
Value = settingDto.Value
|
||||
};
|
||||
|
||||
public static SettingsDto ToSettingsDto(this IEnumerable<Setting> settings) => new SettingsDto {
|
||||
Settings = settings.Select(s => s.ToSettingDto())
|
||||
Value = settingDto.Value,
|
||||
Description = settingDto.Description,
|
||||
Type = settingDto.Type,
|
||||
DisplayType = settingDto.DisplayType
|
||||
};
|
||||
}
|
||||
@@ -19,7 +19,8 @@ public static class UsersMapper {
|
||||
Username = user.Username,
|
||||
CreatedAt = user.CreatedAt,
|
||||
IsBanned = user.BannedAt != null,
|
||||
DeletedAt = user.DeletedAt
|
||||
DeletedAt = user.DeletedAt,
|
||||
AccessLevel = user.AccessLevel,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
457
Lactose/Migrations/20250717171300_limitedStringLenght.Designer.cs
generated
Normal file
457
Lactose/Migrations/20250717171300_limitedStringLenght.Designer.cs
generated
Normal file
@@ -0,0 +1,457 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20250717171300_limitedStringLenght")]
|
||||
partial class limitedStringLenght
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.15")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("BYTEA");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany("SharedWith")
|
||||
.HasForeignKey("AssetId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
|
||||
b.Navigation("SharedWith");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Lactose/Migrations/20250717171300_limitedStringLenght.cs
Normal file
54
Lactose/Migrations/20250717171300_limitedStringLenght.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class limitedStringLenght : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Value",
|
||||
table: "Settings",
|
||||
type: "character varying(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Settings",
|
||||
type: "character varying(50)",
|
||||
maxLength: 50,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Value",
|
||||
table: "Settings",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(1024)",
|
||||
oldMaxLength: 1024);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Name",
|
||||
table: "Settings",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(50)",
|
||||
oldMaxLength: 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
466
Lactose/Migrations/20250718183814_ExpandedSettings.Designer.cs
generated
Normal file
466
Lactose/Migrations/20250718183814_ExpandedSettings.Designer.cs
generated
Normal file
@@ -0,0 +1,466 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Lactose.Context;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
[DbContext(typeof(LactoseDbContext))]
|
||||
[Migration("20250718183814_ExpandedSettings")]
|
||||
partial class ExpandedSettings
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.15")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.Property<Guid>("AlbumsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AlbumsId", "AssetsId");
|
||||
|
||||
b.HasIndex("AssetsId");
|
||||
|
||||
b.ToTable("AlbumAsset");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.Property<Guid>("AssetsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("TagsId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("AssetsId", "TagsId");
|
||||
|
||||
b.HasIndex("TagsId");
|
||||
|
||||
b.ToTable("AssetTag");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("CreatedAt")
|
||||
.IsRequired()
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("PersonOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<Guid?>("UserOwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("PersonOwnerId");
|
||||
|
||||
b.HasIndex("UserOwnerId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<float?>("Duration")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<long>("FileSize")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid?>("FolderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<float?>("FrameRate")
|
||||
.HasColumnType("real");
|
||||
|
||||
b.Property<byte[]>("Hash")
|
||||
.IsRequired()
|
||||
.HasColumnType("BYTEA");
|
||||
|
||||
b.Property<bool>("IsPubliclyShared")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("MimeType")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalFilename")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("OriginalPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<Guid?>("OwnerId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("PreviewPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("ResolutionHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ResolutionWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("ThumbnailPath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FolderId");
|
||||
|
||||
b.HasIndex("OriginalPath")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("OwnerId");
|
||||
|
||||
b.ToTable("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("BoundingBoxX1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxX2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY1")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("BoundingBoxY2")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageHeight")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("ImageWidth")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("PersonId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.HasIndex("PersonId");
|
||||
|
||||
b.ToTable("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("BasePath")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(2048)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Folders");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("People");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
b.ToTable("Settings");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<Guid?>("ParentId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentId");
|
||||
|
||||
b.ToTable("Tags");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessLevel")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid?>("AssetId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime?>("BannedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("DeletedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(128)");
|
||||
|
||||
b.Property<DateTime?>("LastLogin")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Password")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<string>("RefreshToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(255)");
|
||||
|
||||
b.Property<DateTime?>("RefreshTokenExpires")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasColumnType("VARCHAR(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AssetId");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AlbumAsset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Album", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AlbumsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("AssetTag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("AssetsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Lactose.Models.Tag", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("TagsId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Person", "PersonOwner")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("PersonOwnerId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "UserOwner")
|
||||
.WithMany("OwnedAlbums")
|
||||
.HasForeignKey("UserOwnerId");
|
||||
|
||||
b.Navigation("PersonOwner");
|
||||
|
||||
b.Navigation("UserOwner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Folder", "Folder")
|
||||
.WithMany("Assets")
|
||||
.HasForeignKey("FolderId");
|
||||
|
||||
b.HasOne("Lactose.Models.User", "Owner")
|
||||
.WithMany("OwnedAssets")
|
||||
.HasForeignKey("OwnerId");
|
||||
|
||||
b.Navigation("Folder");
|
||||
|
||||
b.Navigation("Owner");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Face", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", "Asset")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("AssetId");
|
||||
|
||||
b.HasOne("Lactose.Models.Person", "Person")
|
||||
.WithMany("Faces")
|
||||
.HasForeignKey("PersonId");
|
||||
|
||||
b.Navigation("Asset");
|
||||
|
||||
b.Navigation("Person");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Tag", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Tag", "Parent")
|
||||
.WithMany()
|
||||
.HasForeignKey("ParentId");
|
||||
|
||||
b.Navigation("Parent");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.HasOne("Lactose.Models.Asset", null)
|
||||
.WithMany("SharedWith")
|
||||
.HasForeignKey("AssetId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Asset", b =>
|
||||
{
|
||||
b.Navigation("Faces");
|
||||
|
||||
b.Navigation("SharedWith");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Folder", b =>
|
||||
{
|
||||
b.Navigation("Assets");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.Person", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Faces");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Lactose.Models.User", b =>
|
||||
{
|
||||
b.Navigation("OwnedAlbums");
|
||||
|
||||
b.Navigation("OwnedAssets");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Lactose/Migrations/20250718183814_ExpandedSettings.cs
Normal file
73
Lactose/Migrations/20250718183814_ExpandedSettings.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Lactose.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ExpandedSettings : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Value",
|
||||
table: "Settings",
|
||||
type: "character varying(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(1024)",
|
||||
oldMaxLength: 1024);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Description",
|
||||
table: "Settings",
|
||||
type: "character varying(2048)",
|
||||
maxLength: 2048,
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DisplayType",
|
||||
table: "Settings",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "Type",
|
||||
table: "Settings",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Description",
|
||||
table: "Settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DisplayType",
|
||||
table: "Settings");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Type",
|
||||
table: "Settings");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "Value",
|
||||
table: "Settings",
|
||||
type: "character varying(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(1024)",
|
||||
oldMaxLength: 1024,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,11 +246,22 @@ namespace Lactose.Migrations
|
||||
modelBuilder.Entity("Lactose.Models.Setting", b =>
|
||||
{
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<int>("DisplayType")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<int>("Type")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.HasKey("Name");
|
||||
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
using Butter.Settings;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Lactose.Models;
|
||||
|
||||
public class Setting {
|
||||
[Key]
|
||||
[Key][MaxLength(50)]
|
||||
public required string Name { get; set; }
|
||||
|
||||
public string Value { get; set; } = string.Empty;
|
||||
[MaxLength(1024)]
|
||||
public string? Value { get; set; }
|
||||
|
||||
[MaxLength(2048)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
public required EType Type { get; set; }
|
||||
|
||||
public required DisplayType DisplayType { get; set; }
|
||||
}
|
||||
|
||||
@@ -63,7 +63,6 @@ AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
// Add services to initialize an empty the database
|
||||
builder.Services.AddScoped<IDbInitializer, DbInitializer>();
|
||||
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddTransient<IPasswordHasher<User>, PasswordHasher<User>>();
|
||||
builder.Services.AddTransient<ISettingsRepository, SettingsRepository>();
|
||||
@@ -75,7 +74,7 @@ builder.Services.AddTransient<IAlbumRepository, AlbumRepository>();
|
||||
builder.Services.AddTransient<IMediaRepository, MediaRepository>();
|
||||
builder.Services.AddTransient<ITokenService, TokenService>();
|
||||
builder.Services.AddTransient<LactoseAuthService>();
|
||||
builder.Services.AddTransient<FileSystemScannerService>();
|
||||
builder.Services.AddSingleton<FileSystemScannerService>();
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddControllers();
|
||||
|
||||
@@ -152,7 +151,7 @@ builder.Services
|
||||
});
|
||||
|
||||
//Add the background scan service
|
||||
//builder.Services.AddHostedService<FileSystemScannerService>();
|
||||
builder.Services.AddHostedService<FileSystemScannerService>();
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
|
||||
@@ -1,28 +1,58 @@
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Lactose.Services;
|
||||
|
||||
namespace Lactose.Repositories;
|
||||
|
||||
public class FolderRepository(LactoseDbContext context) : IFolderRepository {
|
||||
public void Create(Folder folder) => context.Folders.Add(folder);
|
||||
public class FolderRepository(LactoseDbContext context, FileSystemScannerService scanner) : IFolderRepository, IAsyncDisposable {
|
||||
public void Create(Folder folder) {
|
||||
context.Folders.Add(folder);
|
||||
context.SaveChanges();
|
||||
if (folder.Active) scanner.AddFolder(folder);
|
||||
}
|
||||
|
||||
public void Update(Guid id, Folder folder) {
|
||||
var origFolder = context.Folders.Find(id);
|
||||
|
||||
if (origFolder != null) {
|
||||
if (origFolder == null) { return; }
|
||||
|
||||
var oldPath = origFolder;
|
||||
var hasChanged = origFolder.BasePath != folder.BasePath;
|
||||
var wasActive = origFolder.Active;
|
||||
|
||||
|
||||
origFolder.BasePath = folder.BasePath;
|
||||
origFolder.Active = folder.Active;
|
||||
context.Folders.Update(origFolder);
|
||||
context.SaveChanges();
|
||||
|
||||
if (hasChanged && wasActive) {
|
||||
scanner.RemoveFolder(oldPath);
|
||||
}
|
||||
|
||||
if (hasChanged && folder.Active) {}
|
||||
|
||||
if (folder.Active) scanner.AddFolder(origFolder);
|
||||
else scanner.RemoveFolder(origFolder);
|
||||
}
|
||||
|
||||
public void Delete(Guid id) {
|
||||
var folder = context.Folders.Find(id);
|
||||
if(folder != null) context.Folders.Remove(folder);
|
||||
|
||||
if (folder != null) {
|
||||
context.Folders.Remove(folder);
|
||||
scanner.RemoveFolder(folder);
|
||||
}
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
public IEnumerable<Folder> GetAllUntracked() => context.Folders.AsNoTracking();
|
||||
|
||||
public Folder? Get(Guid id) => context.Folders.Find(id);
|
||||
|
||||
public IEnumerable<Folder> GetAll() => context.Folders;
|
||||
|
||||
public void Dispose() => context.Dispose();
|
||||
|
||||
public async ValueTask DisposeAsync() => await context.DisposeAsync();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Lactose.Repositories;
|
||||
/// <summary>
|
||||
/// Interface for folder repository operations.
|
||||
/// </summary>
|
||||
public interface IFolderRepository {
|
||||
public interface IFolderRepository : IDisposable {
|
||||
/// <summary>
|
||||
/// Creates a new folder.
|
||||
/// </summary>
|
||||
@@ -36,4 +36,6 @@ public interface IFolderRepository {
|
||||
/// Retrieves all folders.
|
||||
/// </summary>
|
||||
IEnumerable<Folder> GetAll();
|
||||
|
||||
IEnumerable<Folder> GetAllUntracked();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Lactose.Repositories;
|
||||
/// <summary>
|
||||
/// Interface for managing settings in the repository.
|
||||
/// </summary>
|
||||
public interface ISettingsRepository {
|
||||
public interface ISettingsRepository : IDisposable {
|
||||
/// <summary>
|
||||
/// Creates a settings entry in the repository. If a settings entry already exists, it will be overwritten.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
using Butter.Settings;
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Lactose.Services;
|
||||
using System.Data;
|
||||
|
||||
namespace Lactose.Repositories;
|
||||
|
||||
public class SettingsRepository(LactoseDbContext context) : ISettingsRepository {
|
||||
public class SettingsRepository(LactoseDbContext context, IServiceProvider serviceProvider) : ISettingsRepository {
|
||||
public void Create(Setting setting) {
|
||||
//Check if the settings already exist
|
||||
var existingSettings = context.Settings.FirstOrDefault(s => s.Name == setting.Name);
|
||||
if (existingSettings != null)
|
||||
throw new DuplicateNameException("A setting with the same key already exists: " + setting.Name);
|
||||
|
||||
SettingChange(setting);
|
||||
//Adds the new settings
|
||||
context.Settings.Add(setting);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
public IEnumerable<Setting> Get() => context.Settings.AsEnumerable();
|
||||
@@ -22,20 +26,26 @@ public class SettingsRepository(LactoseDbContext context) : ISettingsRepository
|
||||
public void Update(Setting setting) {
|
||||
var existingSettings = context.Settings.FirstOrDefault(s => s.Name == setting.Name);
|
||||
if (existingSettings != null) {
|
||||
if (existingSettings.Value != setting.Value) SettingChange(setting);
|
||||
existingSettings.Value = setting.Value;
|
||||
context.Settings.Update(existingSettings);
|
||||
context.SaveChanges();
|
||||
} else {
|
||||
// If the settings entry does not exist
|
||||
throw new KeyNotFoundException("The setting with the specified name does not exist: " + setting.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public void Delete() => context.Settings.RemoveRange(context.Settings);
|
||||
public void Delete() {
|
||||
context.Settings.RemoveRange(context.Settings);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
public void Delete(Setting setting) {
|
||||
var existingSettings = context.Settings.FirstOrDefault(s => s.Name == setting.Name);
|
||||
if (existingSettings != null) {
|
||||
context.Settings.Remove(existingSettings);
|
||||
context.SaveChanges();
|
||||
} else {
|
||||
// If the settings entry does not exist
|
||||
throw new KeyNotFoundException("The setting with the specified name does not exist: " + setting.Name);
|
||||
@@ -45,5 +55,25 @@ public class SettingsRepository(LactoseDbContext context) : ISettingsRepository
|
||||
var existingSettings = context.Settings.FirstOrDefault(s => s.Name == name);
|
||||
if (existingSettings == null) throw new KeyNotFoundException("The setting with the specified name does not exist: " + name);
|
||||
Delete(existingSettings);
|
||||
context.SaveChanges();
|
||||
}
|
||||
|
||||
//TODO: May need to be change on a Event based system, because adding here all the Setting Actions could be too much ramification
|
||||
void SettingChange(Setting setting) {
|
||||
switch (setting.Name) {
|
||||
case var name when name == Settings.FolderScanEnabled.AsString():
|
||||
using (var scannerService = serviceProvider.GetService<FileSystemScannerService>()) {
|
||||
if (setting.Value == "false") scannerService?.Disable();
|
||||
else scannerService?.Enable();
|
||||
}
|
||||
return;
|
||||
case var name when name == Settings.FolderScanInterval.AsString():
|
||||
using (var scannerService = serviceProvider.GetService<FileSystemScannerService>()) {
|
||||
if (int.TryParse(setting.Value, out var interval)) scannerService?.SetTimerInterval(TimeSpan.FromMinutes(interval));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => context.Dispose();
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
using Butter;
|
||||
using Butter.Settings;
|
||||
using Butter.Types;
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Lactose.Services;
|
||||
|
||||
@@ -9,23 +13,23 @@ interface IDbInitializer {
|
||||
void Initialize();
|
||||
}
|
||||
|
||||
class DbInitializer(
|
||||
LactoseDbContext dbContext,
|
||||
IPasswordHasher<User> passwordHasher
|
||||
): IDbInitializer
|
||||
{
|
||||
class DbInitializer(LactoseDbContext dbContext, IPasswordHasher<User> passwordHasher) : IDbInitializer {
|
||||
public void Initialize() {
|
||||
ArgumentNullException.ThrowIfNull(dbContext, nameof(dbContext));
|
||||
|
||||
var task = dbContext.Database.GetPendingMigrationsAsync();
|
||||
|
||||
task.ContinueWith(
|
||||
(r) => {
|
||||
task.ContinueWith((r) => {
|
||||
if (r.Result.Any()) dbContext.Database.Migrate();
|
||||
}
|
||||
).Wait();
|
||||
)
|
||||
.Wait();
|
||||
|
||||
// if(dbContext.Database.GetPendingMigrations().Any()) // Timeoout
|
||||
if (dbContext.Users.Any()) return;
|
||||
|
||||
#region Users
|
||||
|
||||
if (!dbContext.Users.Any()) {
|
||||
var user = new User {
|
||||
Username = "admin",
|
||||
Email = "admin@admin.com",
|
||||
@@ -33,8 +37,52 @@ class DbInitializer(
|
||||
CreatedAt = DateTime.Now,
|
||||
UpdatedAt = DateTime.Now
|
||||
};
|
||||
|
||||
user.Password = passwordHasher.HashPassword(user, "admin");
|
||||
dbContext.Users.Add(user);
|
||||
dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Settings
|
||||
|
||||
var settings = System.Text.Json.JsonSerializer.Deserialize<List<Setting>>(File.ReadAllText("DefaultSettings.json"));
|
||||
|
||||
if (settings is not null) {
|
||||
//remove all settings that are not in the default settings file
|
||||
var existingSettings = dbContext.Settings.ToList();
|
||||
foreach (Setting existingSetting in existingSettings
|
||||
.Where(existingSetting => settings.All(s => s.Name != existingSetting.Name)))
|
||||
dbContext.Settings.Remove(existingSetting);
|
||||
// add or update settings from the default settings file leaving value unchanged
|
||||
foreach (var setting in settings) {
|
||||
if (!dbContext.Settings.Any(s => s.Name == setting.Name)) { dbContext.Settings.Add(setting); } else {
|
||||
var dbSetting = dbContext.Settings.First(s => s.Name == setting.Name);
|
||||
dbSetting.Description = setting.Description;
|
||||
dbSetting.Type = setting.Type;
|
||||
dbSetting.DisplayType = setting.DisplayType;
|
||||
dbContext.Settings.Update(dbSetting);
|
||||
}
|
||||
}
|
||||
dbContext.SaveChanges();
|
||||
} //else, we are a bit fucked.
|
||||
|
||||
#endregion
|
||||
|
||||
#region Folders
|
||||
|
||||
if (!dbContext.Folders.Any()) {
|
||||
dbContext.Folders.Add(
|
||||
new Folder() {
|
||||
BasePath = "/diary",
|
||||
Active = false
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
dbContext.SaveChanges();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,171 +1,120 @@
|
||||
using Butter;
|
||||
using Butter.Types;
|
||||
using Butter.Dtos.Settings;
|
||||
using Butter.Settings;
|
||||
using Lactose.Context;
|
||||
using Lactose.Models;
|
||||
using Lactose.Repositories;
|
||||
using Lactose.Utils;
|
||||
using System.Data;
|
||||
using System.Diagnostics;
|
||||
using static System.String;
|
||||
|
||||
namespace Lactose.Services;
|
||||
|
||||
public class FileSystemScannerService(
|
||||
ILogger<FileSystemScannerService> logger,
|
||||
IAssetRepository assetRepository,
|
||||
IFolderRepository folderRepository,
|
||||
ISettingsRepository settingsRepository
|
||||
) : BackgroundService {
|
||||
public bool IsRunning { get; private set; } = false;
|
||||
public class FileSystemScannerService(ILogger<FileSystemScannerService> logger, IServiceProvider serviceProvider)
|
||||
: BackgroundService {
|
||||
public bool IsScanning { get; private set; } = false;
|
||||
public bool IsEnabled { get; private set; } = false;
|
||||
public bool IsInitialized { get; private set; } = false;
|
||||
public TimeSpan ScanInterval { get; private set; }
|
||||
List<Folder> folders = [];
|
||||
readonly PeriodicTimer scanTimer = new(TimeSpan.FromMinutes(1));
|
||||
readonly Dictionary<string, CancellationTokenSource> folders = new();
|
||||
readonly List<Task> tasks = [];
|
||||
|
||||
// ReSharper disable ArrangeObjectCreationWhenTypeEvident
|
||||
static readonly MimeTypeMap[] ImageMime = [
|
||||
new("image/avif", [".avif", ".avifs"]),
|
||||
new("image/bmp}", [".bmp"]),
|
||||
new("image/cgm", [".cgm"]),
|
||||
new("image/g3fax", [".g3"]),
|
||||
new("image/gif", [".gif"]),
|
||||
new("image/heic", [".heif", ".heic"]),
|
||||
new("image/ief", [".ief"]),
|
||||
new("image/jpeg", [".jpe", ".jpeg", ".jpg", ".pjpg", ".jfif", ".jfif-tbnl", ".jif"]),
|
||||
new("image/pjpeg", [".jpe", ".jpeg", ".jpg", ".pjpg", ".jfi", ".jfif", ".jfif-tbnl", ".jif"]),
|
||||
new("image/png", [".png"]),
|
||||
new("image/prs.btif", [".btif"]),
|
||||
new("image/svg+xml", [".svg", ".svgz"]),
|
||||
new("image/tiff", [".tif", ".tiff"]),
|
||||
new("image/vnd.adobe.photoshop", [".psd"]),
|
||||
new("image/vnd.djvu", [".djv", ".djvu"]),
|
||||
new("image/vnd.dwg", [".dwg"]),
|
||||
new("image/vnd.dxf", [".dxf"]),
|
||||
new("image/vnd.fastbidsheet", [".fbs"]),
|
||||
new("image/vnd.fpx", [".fpx"]),
|
||||
new("image/vnd.fst", [".fst"]),
|
||||
new("image/vnd.fujixerox.edmics-mmr", [".mmr"]),
|
||||
new("image/vnd.fujixerox.edmics-rlc", [".rlc"]),
|
||||
new("image/vnd.ms-modi", [".mdi"]),
|
||||
new("image/vnd.net-fpx", [".npx"]),
|
||||
new("image/vnd.wap.wbmp", [".wbmp"]),
|
||||
new("image/vnd.xiff", [".xif"]),
|
||||
new("image/webp", [".webp"]),
|
||||
new("image/x-adobe-dng", [".dng"]),
|
||||
new("image/x-canon-cr2", [".cr2"]),
|
||||
new("image/x-canon-crw", [".crw"]),
|
||||
new("image/x-cmu-raster", [".ras"]),
|
||||
new("image/x-cmx", [".cmx"]),
|
||||
new("image/x-epson-erf", [".erf"]),
|
||||
new("image/x-freehand", [".fh", ".fh4", ".fh5", ".fh7", ".fhc"]),
|
||||
new("image/x-fuji-raf", [".raf"]),
|
||||
new("image/x-icns", [".icns"]),
|
||||
new("image/x-icon", [".ico"]),
|
||||
new("image/x-kodak-dcr", [".dcr"]),
|
||||
new("image/x-kodak-k25", [".k25"]),
|
||||
new("image/x-kodak-kdc", [".kdc"]),
|
||||
new("image/x-minolta-mrw", [".mrw"]),
|
||||
new("image/x-nikon-nef", [".nef"]),
|
||||
new("image/x-olympus-orf", [".orf"]),
|
||||
new("image/x-panasonic-raw", [".raw", ".rw2", ".rwl"]),
|
||||
new("image/x-pcx", [".pcx"]),
|
||||
new("image/x-pentax-pef", [".pef", ".ptx"]),
|
||||
new("image/x-pict", [".pct", ".pic"]),
|
||||
new("image/x-portable-anymap", [".pnm"]),
|
||||
new("image/x-portable-bitmap", [".pbm"]),
|
||||
new("image/x-portable-graymap", [".pgm"]),
|
||||
new("image/x-portable-pixmap", [".ppm"]),
|
||||
new("image/x-rgb", [".rgb"]),
|
||||
new("image/x-sigma-x3f", [".x3f"]),
|
||||
new("image/x-sony-arw", [".arw"]),
|
||||
new("image/x-sony-sr2", [".sr2"]),
|
||||
new("image/x-sony-srf", [".srf"]),
|
||||
new("image/x-xbitmap", [".xbm"]),
|
||||
new("image/x-xpixmap", [".xpm"]),
|
||||
new("image/x-xwindowdump", [".xwd"])
|
||||
];
|
||||
|
||||
static readonly MimeTypeMap[] VideoMime = [
|
||||
new("video/3gpp", [".3gp"]),
|
||||
new("video/3gpp2", [".3g2"]),
|
||||
new("video/h261", [".h261"]),
|
||||
new("video/h263", [".h263"]),
|
||||
new("video/h264", [".h264"]),
|
||||
new("video/jpeg", [".jpgv"]),
|
||||
new("video/jpm", [".jpgm, .jpm"]),
|
||||
new("video/mj2", [".mj2, .mjp2"]),
|
||||
new("video/mp2t", [".ts"]),
|
||||
new("video/mp4", [".mp4, .mp4v, .mpg4"]),
|
||||
new("video/mpeg", [".m1v, .m2v, .mpa, .mpe, .mpeg, .mpg"]),
|
||||
new("video/ogg", [".ogv"]),
|
||||
new("video/quicktime", [".mov", ".qt"]),
|
||||
new("video/vnd.fvt", [".fvt"]),
|
||||
new("video/vnd.mpegurl", [".m4u", ".mxu"]),
|
||||
new("video/vnd.ms-playready.media.pyv", [".pyv"]),
|
||||
new("video/vnd.vivo", [".viv"]),
|
||||
new("video/webm", [".webm"]),
|
||||
new("video/x-f4v", [".f4v"]),
|
||||
new("video/x-fli", [".fli"]),
|
||||
new("video/x-flv", [".flv"]),
|
||||
new("video/x-m4v", [".m4v"]),
|
||||
new("video/x-matroska", [".mkv"]),
|
||||
new("video/x-ms-asf", [".asf", ".asx"]),
|
||||
new("video/x-ms-wm", [".wm"]),
|
||||
new("video/x-ms-wmv", [".wmv"]),
|
||||
new("video/x-ms-wmx", [".wmx"]),
|
||||
new("video/x-ms-wvx", [".wvx"]),
|
||||
new("video/x-msvideo", [".avi"]),
|
||||
new("video/x-sgi-movie", [".movie"])
|
||||
];
|
||||
|
||||
protected override Task ExecuteAsync(CancellationToken stoppingToken) {
|
||||
if(settingsRepository.Get(Settings.FolderScanEnabled.AsString())?.Value == "false") {
|
||||
logger.LogInformation("Folder scanning is disabled. Service will not start.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
var service = new Task(async void () => await TaskWatcher(stoppingToken));
|
||||
var lifetime = serviceProvider.GetRequiredService<IHostApplicationLifetime>();
|
||||
|
||||
var service = new Task(() => ServiceLogic(stoppingToken));
|
||||
service.Start();
|
||||
lifetime.ApplicationStarted.Register(() => service.Start());
|
||||
return service;
|
||||
}
|
||||
|
||||
//TODO: This really needs a decent name.
|
||||
void ServiceLogic(CancellationToken stoppingToken) {
|
||||
PeriodicTimer scanTimer;
|
||||
async Task TaskWatcher(CancellationToken stoppingToken) {
|
||||
// Resolve the repositories from the service provider
|
||||
var scope = serviceProvider.CreateScope();
|
||||
var settingsRepository = scope.ServiceProvider.GetRequiredService<ISettingsRepository>();
|
||||
IsEnabled = settingsRepository.Get(Settings.FolderScanEnabled.AsString())?.Value != "false";
|
||||
|
||||
stoppingToken.Register(() => {
|
||||
logger.LogInformation("Filesystem Scan Service stopping...");
|
||||
|
||||
folders.Values.Where(tokenSource => !tokenSource.IsCancellationRequested)
|
||||
.ForEach(tokenSource => {
|
||||
tokenSource.Cancel();
|
||||
tokenSource.Dispose();
|
||||
}
|
||||
);
|
||||
|
||||
folders.Clear();
|
||||
}
|
||||
);
|
||||
|
||||
var folderRepository = scope.ServiceProvider.GetRequiredService<IFolderRepository>();
|
||||
folderRepository.GetAllUntracked().Where(f => f.Active).Select(f => f.BasePath).ForEach(AddFolder);
|
||||
|
||||
logger.LogInformation("Filesystem Scan Service starting...");
|
||||
|
||||
var value = settingsRepository.Get(Settings.FolderScanInterval.AsString())?.Value;
|
||||
|
||||
if (value != null && !int.TryParse(value, out var interval)) {
|
||||
logger.LogWarning("Invalid scan interval setting. Defaulting to 30 minutes.");
|
||||
interval = 30; // Default to 30 minutes if parsing fails
|
||||
} else interval = int.Parse(value!);
|
||||
|
||||
ScanInterval = TimeSpan.FromMinutes(interval);
|
||||
logger.LogInformation($"Service will scan every {ScanInterval.TotalMinutes} minutes.");
|
||||
scanTimer = new (ScanInterval);
|
||||
SetTimerInterval(TimeSpan.FromMinutes(interval));
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested) {
|
||||
logger.LogInformation("Retrieving folder paths to scan...");
|
||||
folders = folderRepository.GetAll().ToList();
|
||||
|
||||
// Wait for all tasks to complete before starting a new scan
|
||||
if(tasks.Count > 0) {
|
||||
if (IsScanning) {
|
||||
logger.LogInformation("Waiting for {count} tasks to complete...", tasks.Count);
|
||||
Task.WaitAll(tasks.ToArray(), stoppingToken);
|
||||
IsRunning = false;
|
||||
WaitTasks(stoppingToken);
|
||||
}
|
||||
|
||||
if (folders.Count != 0) {
|
||||
if (IsEnabled && folders.Count != 0 && !IsScanning) {
|
||||
logger.LogInformation("Found {count} folders to scan.", folders.Count);
|
||||
logger.LogInformation("Starting Tasks for scanning folders...");
|
||||
|
||||
// Launch a folder scan for each folder, passing the stopping token to each task
|
||||
folders.ForEach(folder => { tasks.Add(Task.Run(() => ScanFolder(folder.BasePath), stoppingToken)); });
|
||||
IsRunning = true;
|
||||
} else {
|
||||
logger.LogWarning("No folders to scan. Waiting for next scan interval...");
|
||||
scanTimer.WaitForNextTickAsync(stoppingToken);
|
||||
folders.Where(entry => !entry.Value.IsCancellationRequested)
|
||||
.Select(entry => entry.Key)
|
||||
.ToList()
|
||||
.ForEach(path => { folders[path] = new CancellationTokenSource(); });
|
||||
|
||||
folders.ForEach((entry)=>
|
||||
tasks.Add(Task.Run(() => ScanFolder(entry.Key, entry.Value.Token), entry.Value.Token))
|
||||
);
|
||||
|
||||
IsScanning = true;
|
||||
await scanTimer.WaitForNextTickAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScanFolder(string path) {
|
||||
void WaitTasks(CancellationToken stoppingToken) {
|
||||
while (IsScanning) {
|
||||
try {
|
||||
Task.WaitAll(tasks.ToArray(), stoppingToken);
|
||||
tasks.Clear();
|
||||
IsScanning = false;
|
||||
} catch (OperationCanceledException e) {
|
||||
logger.LogInformation("Scanning Service was cancelled.");
|
||||
IsScanning = false;
|
||||
IsEnabled = false;
|
||||
} catch (AggregateException e) {
|
||||
if (!e.InnerExceptions.All(ie => ie is OperationCanceledException)) throw;
|
||||
|
||||
logger.LogDebug("A Folder Has been removed. Continuing to scan.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScanFolder(string path, CancellationToken stoppingToken) {
|
||||
|
||||
if (stoppingToken.IsCancellationRequested) { stoppingToken.ThrowIfCancellationRequested(); };
|
||||
|
||||
List<string> filePaths = [];
|
||||
List<string> folderPaths = [];
|
||||
|
||||
@@ -176,14 +125,34 @@ public class FileSystemScannerService(
|
||||
} catch (Exception e) { logger.LogError(e, "Error scanning folder {folder}", path); }
|
||||
|
||||
// Launch a folder scan for each subfolder
|
||||
folderPaths.ForEach(folderPath => { tasks.Add(Task.Run(() => ScanFolder(folderPath))); });
|
||||
folderPaths.ForEach(folderPath => {
|
||||
logger.LogDebug("Adding subfolder {folder} to scan queue.", folderPath);
|
||||
tasks.Add(Task.Run(() => ScanFolder(folderPath, stoppingToken), stoppingToken));
|
||||
});
|
||||
var scope = serviceProvider.CreateScope();
|
||||
var assetRepository = scope.ServiceProvider.GetRequiredService<IAssetRepository>();
|
||||
|
||||
// Process all files
|
||||
filePaths.ForEach(
|
||||
filePath => {
|
||||
filePaths.ForEach(filePath => {
|
||||
// Check if the file is already in the database
|
||||
if (assetRepository.FindByPath(filePath) != null) return;
|
||||
|
||||
var asset = AssetFromPath(filePath);
|
||||
|
||||
if (asset == null) return;
|
||||
// Add the asset to the database
|
||||
try {
|
||||
assetRepository.Insert(asset);
|
||||
assetRepository.Save();
|
||||
} catch (DuplicateNameException e) {
|
||||
logger.LogError(e, $"Duplicate asset name \"{asset.OriginalFilename}\", skipped.");
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Asset? AssetFromPath(string filePath) {
|
||||
logger.LogTrace("Loading asset from file path {filePath}", filePath);
|
||||
// Get the file info
|
||||
var finfo = new FileInfo(filePath);
|
||||
|
||||
@@ -192,15 +161,16 @@ public class FileSystemScannerService(
|
||||
EAssetType? type;
|
||||
|
||||
// ReSharper disable ArrangeObjectCreationWhenTypeNotEvident
|
||||
var mimeImg = ImageMime.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
|
||||
var mimeVid = VideoMime.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
|
||||
var mimeImg = MimeTypes.Image.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
|
||||
var mimeVid = MimeTypes.Video.FirstOrDefault(mime => mime.Extensions.Contains(ext), new(Empty, []));
|
||||
|
||||
if (mimeImg.MimeType != Empty) type = EAssetType.Image;
|
||||
else if (mimeVid.MimeType != Empty) type = EAssetType.Video;
|
||||
else return;
|
||||
else return null;
|
||||
|
||||
// Create a new asset
|
||||
var asset = new Asset() {
|
||||
//TODO: folder ID is missing here
|
||||
OriginalPath = finfo.FullName,
|
||||
OriginalFilename = finfo.Name,
|
||||
Type = type.Value,
|
||||
@@ -216,17 +186,56 @@ public class FileSystemScannerService(
|
||||
Hash = []
|
||||
};
|
||||
|
||||
//TODO: check if the file is already in the database
|
||||
// Add the asset to the database
|
||||
try { assetRepository.Insert(asset); }
|
||||
catch (DuplicateNameException e) {
|
||||
logger.LogError(e, $"Duplicate asset name \"{finfo.FullName}\", skipped."); }
|
||||
}
|
||||
);
|
||||
}
|
||||
logger.LogDebug(
|
||||
$"""
|
||||
Asset created from path: {asset.OriginalPath}
|
||||
Data:
|
||||
- Filename: {asset.OriginalFilename}
|
||||
- Type: {asset.Type}
|
||||
- MimeType: {asset.MimeType}
|
||||
- CreatedAt: {asset.CreatedAt}
|
||||
- UpdatedAt: {asset.UpdatedAt}
|
||||
- FileSize: {asset.FileSize} bytes
|
||||
""");
|
||||
return asset;
|
||||
}
|
||||
|
||||
public struct MimeTypeMap(string mimeType, string[] extensions) {
|
||||
public string MimeType { get; set; } = mimeType;
|
||||
public string[] Extensions { get; set; } = extensions;
|
||||
public void SetTimerInterval(TimeSpan interval) {
|
||||
scanTimer.Period = interval;
|
||||
logger.LogInformation($"Service will scan every {scanTimer.Period.TotalMinutes} minutes.");
|
||||
}
|
||||
|
||||
public void AddFolder(string folder) {
|
||||
if (folders.ContainsKey(folder)) {
|
||||
logger.LogWarning("Folder {folder} already exists", folder);
|
||||
return;
|
||||
}
|
||||
|
||||
folders.Add(folder, new CancellationTokenSource());
|
||||
logger.LogInformation("Added folder {folder}", folder);
|
||||
|
||||
}
|
||||
|
||||
public void AddFolder(Folder folder) => AddFolder(folder.BasePath);
|
||||
|
||||
public void RemoveFolder(string folder) {
|
||||
if (!folders.TryGetValue(folder, out CancellationTokenSource? cancellationToken)) {
|
||||
logger.LogWarning("Folder {folder} does not exist", folder);
|
||||
return;
|
||||
}
|
||||
|
||||
cancellationToken.Cancel();
|
||||
cancellationToken.Dispose();
|
||||
folders.Remove(folder);
|
||||
logger.LogInformation("Removed folder {folder}", folder);
|
||||
}
|
||||
|
||||
public void RemoveFolder(Folder folder) => RemoveFolder(folder.BasePath);
|
||||
|
||||
public void Enable() {
|
||||
IsEnabled = true;
|
||||
logger.LogInformation("Service is now enabled.");
|
||||
}
|
||||
|
||||
public void Disable() => IsEnabled = false;
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
namespace Lactose;
|
||||
|
||||
public enum Settings {
|
||||
UserRegistrationEnabled, //Enable or disable self user registration
|
||||
FolderScanEnabled, // Used to enable/disable folder scanning
|
||||
FolderScanInterval, //Interval in minutes for folder scanning
|
||||
FileUploadEnabled, //Enable or disable file upload
|
||||
FileUploadMaxSize, //Maximum file size for uploads in bytes
|
||||
}
|
||||
|
||||
public static class SettingsExtensions {
|
||||
public static string AsString(this Settings setting) {
|
||||
return setting switch {
|
||||
Settings.UserRegistrationEnabled => "UserRegistrationEnabled",
|
||||
Settings.FolderScanEnabled => "FolderScanEnabled",
|
||||
Settings.FolderScanInterval => "FolderScanInterval",
|
||||
Settings.FileUploadEnabled => "FileUploadEnabled",
|
||||
Settings.FileUploadMaxSize => "FileUploadMaxSize",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(setting), setting, null)
|
||||
};
|
||||
}
|
||||
|
||||
public static Type AsValueType(this Settings setting) {
|
||||
return setting switch {
|
||||
Settings.UserRegistrationEnabled => typeof(bool),
|
||||
Settings.FolderScanEnabled => typeof(bool),
|
||||
Settings.FolderScanInterval => typeof(int),
|
||||
Settings.FileUploadEnabled => typeof(bool),
|
||||
Settings.FileUploadMaxSize => typeof(int),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(setting), setting, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
10
Lactose/Utils/EnumerableExtensions.cs
Normal file
10
Lactose/Utils/EnumerableExtensions.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Lactose.Utils;
|
||||
|
||||
public static class EnumerableExtensions {
|
||||
public static void ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> action) {
|
||||
|
||||
foreach (var item in source) {
|
||||
action(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@
|
||||
"DatabaseCredentials": {
|
||||
"UserID": "root",
|
||||
"Password": "testOnlyDb",
|
||||
"Database": "TestDb"
|
||||
"Database": "TestDb",
|
||||
"connString":"Server=127.0.0.1;Port=3306;Database=TestDb;User Id=root;Password=testOnlyDb;"
|
||||
},
|
||||
"DatabaseAddress": {
|
||||
"Host": "localhost",
|
||||
|
||||
17
MilkStream/AuthHelpers.cs
Normal file
17
MilkStream/AuthHelpers.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Butter.Dtos;
|
||||
|
||||
namespace MilkStream;
|
||||
|
||||
public static class AuthHelpers {
|
||||
public static AuthInfo ToAuthInfo(this AuthResultDto authResult) {
|
||||
if (authResult == null) {
|
||||
throw new ArgumentNullException(nameof(authResult), "AuthResultDto cannot be null");
|
||||
}
|
||||
|
||||
return new () {
|
||||
UserId = authResult.UserId,
|
||||
Token = authResult.Token,
|
||||
RefreshToken = authResult.RefreshToken
|
||||
};
|
||||
}
|
||||
}
|
||||
7
MilkStream/AuthInfo.cs
Normal file
7
MilkStream/AuthInfo.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MilkStream;
|
||||
|
||||
public class AuthInfo {
|
||||
public required Guid? UserId { get; set; }
|
||||
public required string? Token { get; set; }
|
||||
public required string? RefreshToken { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-bs-theme="dark" lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
@@ -12,13 +12,15 @@
|
||||
@* ReSharper disable once Html.PathError *@
|
||||
<link rel="stylesheet" href="MilkStream.styles.css"/>
|
||||
@* the above one is the autogenerated css from the component isolations *@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<HeadOutlet @rendermode="InteractiveServer"/>
|
||||
<HeadOutlet @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
||||
<title>Milkstream</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes @rendermode="InteractiveServer"/>
|
||||
<Routes @rendermode="new InteractiveServerRenderMode(prerender: false)" />
|
||||
<script src="lib/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
59
MilkStream/Components/FolderBox.razor
Normal file
59
MilkStream/Components/FolderBox.razor
Normal file
@@ -0,0 +1,59 @@
|
||||
@using Butter.Dtos.Folder
|
||||
@using MilkStream.Services
|
||||
@inject FoldersService FoldersService
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-body align-items-center">
|
||||
<div class="d-flex flex-row flex-grow-1 align-items-center">
|
||||
<i class="bi bi-folder2 ms-3 mx-1"></i><p class="mx-1 mb-0 @Display">@(Folder.BasePath ?? "Unknown Folder")</p>
|
||||
<input class="form-control form-control-sm mx-1 @Edit" type="text" @bind="Folder.BasePath"/>
|
||||
<button class="btn btn-sm btn-success @Edit" @onclick="OnConfirmEdit"><i class="bi bi-check"></i></button>
|
||||
<div class="ms-auto form-switch">
|
||||
<input class="form-check-input m-1" type="checkbox" id="@Folder.Id" @bind="@Folder.Active"/>
|
||||
</div>
|
||||
<button class="btn btn-outline-warning btn-sm @Display" @onclick="OnEdit"><i class="bi bi-pencil"></i></button>
|
||||
<button class="btn btn-outline-danger btn-sm mx-1" @onclick="OnDelete"><i class="bi bi-folder-minus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
bool editing = false;
|
||||
string Display => editing ? "d-none" : "";
|
||||
string Edit => editing ? "" : "d-none";
|
||||
|
||||
[Parameter]
|
||||
public FolderFullDto Folder { get; set; } = new FolderFullDto();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
FoldersService.SaveFolders += (_, _) => FoldersService.UpdateFolder(
|
||||
Folder.Id,
|
||||
new FolderUpdateDto() {
|
||||
Active = Folder.Active,
|
||||
BasePath = Folder.BasePath
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void OnEdit() {
|
||||
editing = true;
|
||||
}
|
||||
|
||||
void OnConfirmEdit() {
|
||||
FoldersService.UpdateFolder(
|
||||
Folder.Id,
|
||||
new FolderUpdateDto() {
|
||||
Active = Folder.Active,
|
||||
BasePath = Folder.BasePath
|
||||
}
|
||||
);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
void OnDelete() {
|
||||
FoldersService.RemoveFolder(Folder.Id);
|
||||
NavigationManager.Refresh(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +1,15 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
<div class="sidebar">
|
||||
<NavMenu/>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<article class="content px-4">
|
||||
<main class="container-xxl d-flex flex-grow-1">
|
||||
<div class="d-flex flex-column border border-1 border-opacity-50 border-secondary-subtle rounded-3 m-lg-5 m-1 p-lg-5 p-1 flex-grow-1">
|
||||
@Body
|
||||
</article>
|
||||
</main>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="container-xxl footer d-flex justify-content-center">
|
||||
<span class="text-primary m-auto">Footer (duh!)</span>
|
||||
</footer>
|
||||
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
|
||||
@@ -1,81 +1,6 @@
|
||||
.page{
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, rgb(174, 18, 213) 90%);
|
||||
color: lightgray;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1px solid #d6d5d5;
|
||||
justify-content: flex-end;
|
||||
height: 3.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
white-space: nowrap;
|
||||
margin-left: 1.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.top-row ::deep a:first-child {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 640.98px) {
|
||||
.top-row {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.top-row ::deep a, .top-row ::deep .btn-link {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.page {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
height: 100vh;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.top-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.top-row.auth ::deep a:first-child {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.top-row, article {
|
||||
padding-left: 2rem !important;
|
||||
padding-right: 1.5rem !important;
|
||||
}
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
#blazor-error-ui {
|
||||
|
||||
@@ -1,25 +1,115 @@
|
||||
<div class="top-row ps-3 navbar navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<div>
|
||||
<a class="navbar-brand" href="">
|
||||
<img src="MilkyShotLogoWhite.svg" alt="Milky Shot Logo" class="logo" href=""/>
|
||||
Milky Shot
|
||||
@using MilkStream.Services
|
||||
@using Butter.Types
|
||||
|
||||
@inherits LayoutComponentBase
|
||||
@inject NavigationManager navigation
|
||||
@inject UserService userService
|
||||
@inject LoginService loginService
|
||||
@inject ProtectedLocalStorage localStorage
|
||||
|
||||
<div class="navbar navbar-expand-sm bg-dark-subtle">
|
||||
<div class="container-xl">
|
||||
<a class="mx-2 navbar-brand" href="">
|
||||
<img src="img/MilkyShotLogoWhite.svg" alt="Milky Shot Logo" class="logo mx-2" href=""/>
|
||||
<span class="text m-1">Milky Shot</span>
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Cosplayers</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Albums</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Photos</a>
|
||||
</li>
|
||||
</ul>
|
||||
@if (isConnected) {
|
||||
if (!LoggedIn) {
|
||||
<button class="btn btn-primary mx-1" @onclick="OnLoginClick">
|
||||
Login
|
||||
</button>
|
||||
<button class="btn btn-primary mx-1" @onclick="OnRegisterClick">
|
||||
Register
|
||||
</button>
|
||||
} else {
|
||||
<div class="d-flex">
|
||||
<input class="form-control form-control-sm ms-1"
|
||||
style="border-top-right-radius: 0; border-bottom-right-radius: 0" type="search"
|
||||
placeholder="Search anything..." aria-label="search"/>
|
||||
<button class="btn btn-sm btn-outline-success me-1"
|
||||
style="border-top-left-radius: 0; border-bottom-left-radius: 0" type="submit">
|
||||
<i class="bi bi-search-heart"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mx-2">@loginService.LoggedUser?.Username</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-lg btn-outline-light" type="button">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
</button>
|
||||
<button class="btn btn-lg btn-outline-light dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown"></button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="#"><i class="bi bi-person-lines-fill"></i> Profile</a></li>
|
||||
@if(Admin) {
|
||||
<li><a class="dropdown-item" @onclick="OnSettingsClick"><i class="bi bi-gear-wide-connected"></i> Settings</a></li>
|
||||
}
|
||||
<li><a class="dropdown-item" @onclick="OnLogout"><i class="bi bi-door-closed"></i> Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" title="Navigation menu" class="navbar-toggler"/>
|
||||
@code{
|
||||
bool LoggedIn => loginService.IsLoggedIn;
|
||||
bool Admin => loginService.LoggedUser?.AccessLevel == EAccessLevel.Admin;
|
||||
bool isConnected;
|
||||
bool needsUpdate;
|
||||
|
||||
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
|
||||
<nav class="flex-column">
|
||||
<div class="nav-item px-3">
|
||||
<NavLink class="nav-link" href="/login">
|
||||
<span class="bi" aria-hidden="true"></span> Login
|
||||
</NavLink>
|
||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
|
||||
</NavLink>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
protected override void OnInitialized() {
|
||||
base.OnInitialized();
|
||||
loginService.LoggedUserChanged += (sender, dto) => needsUpdate = true;
|
||||
LoadStateAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
protected async override Task OnAfterRenderAsync(bool firstRender) {
|
||||
if (firstRender || needsUpdate) {
|
||||
isConnected = true;
|
||||
await LoadStateAsync();
|
||||
needsUpdate = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadStateAsync() {
|
||||
var auth = await localStorage.GetAsync<AuthInfo>("auth");
|
||||
if (auth.Success == false) return;
|
||||
loginService.AuthInfo = auth.Value;
|
||||
loginService.LoggedUser = await userService.GetUserAsync();//.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
async Task OnLoginClick() {
|
||||
loginService.LoggedUser = await userService.GetUserAsync();
|
||||
|
||||
if (loginService.LoggedUser != null) needsUpdate = true;
|
||||
else navigation.NavigateTo("/login");
|
||||
}
|
||||
|
||||
void OnRegisterClick() => navigation.NavigateTo("/register");
|
||||
|
||||
void OnSettingsClick() => navigation.NavigateTo("/settings");
|
||||
|
||||
async Task OnLogout() {
|
||||
_ = loginService.Logout();
|
||||
await localStorage.DeleteAsync("auth");
|
||||
navigation.NavigateTo("/", true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +1,12 @@
|
||||
.navbar-toggler {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
width: 3.5rem;
|
||||
height: 2.5rem;
|
||||
color: white;
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 1rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-toggler:checked {
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.top-row {
|
||||
height: 3.5rem;
|
||||
background-color: rgba(0,0,0,0.4);
|
||||
.navbar{
|
||||
box-shadow: 0 0.05rem 0.3rem var(--bs-secondary-bg-subtle);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.1rem;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
filter: drop-shadow(0 0 0.3rem var(--bs-light-border-subtle));
|
||||
}
|
||||
|
||||
.logo{
|
||||
@@ -31,83 +14,3 @@
|
||||
height: 2.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bi {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
margin-right: 0.75rem;
|
||||
top: -1px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.bi-house-door-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-plus-square-fill-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.bi-list-nested-nav-menu {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 0.9rem;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.nav-item:first-of-type {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.nav-item:last-of-type {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link {
|
||||
color: #d7d7d7;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 3rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav-item ::deep a.active {
|
||||
background-color: rgba(255,255,255,0.37);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-item ::deep .nav-link:hover {
|
||||
background-color: rgba(255,255,255,0.1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-toggler:checked ~ .nav-scrollable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (min-width: 641px) {
|
||||
.navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-scrollable {
|
||||
/* Never collapse the sidebar for wide screens */
|
||||
display: block;
|
||||
|
||||
/* Allow sidebar to scroll for tall menus */
|
||||
height: calc(100vh - 3.5rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
<div class="spinner-container m-5">
|
||||
<div class="spinner-border m-2">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div class="d-flex align-items-center flex-column m-5">
|
||||
<div class="spinner-border text-info m-2"></div>
|
||||
<div class="m-2">
|
||||
<strong class="text">
|
||||
<span>L</span>
|
||||
<span>o</span>
|
||||
<span>a</span>
|
||||
<span>d</span>
|
||||
<span>i</span>
|
||||
<span>n</span>
|
||||
<span>g</span>
|
||||
<span> </span>
|
||||
<span>d</span>
|
||||
<span>a</span>
|
||||
<span>t</span>
|
||||
<span>a</span>
|
||||
<span>.</span>
|
||||
<span>.</span>
|
||||
<span>.</span>
|
||||
<strong class="text-info-emphasis">
|
||||
Loading data...
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,52 +0,0 @@
|
||||
.spinner-container{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.spinner-container ::deep .spinner-border {
|
||||
}
|
||||
|
||||
.spinner-container ::deep .text {
|
||||
color: var(--main-col);
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.spinner-container ::deep .animated-text {
|
||||
animation: ease-in-out 1s linear infinite;
|
||||
animation-direction: normal;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@keyframes jump {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
}
|
||||
|
||||
.text span {
|
||||
display: inline-block;
|
||||
animation: jump 1.5s ease-in-out 1s infinite;
|
||||
}
|
||||
|
||||
.text span:nth-child(1) { animation-delay: 0.0s; }
|
||||
.text span:nth-child(2) { animation-delay: 0.1s; }
|
||||
.text span:nth-child(3) { animation-delay: 0.2s; }
|
||||
.text span:nth-child(4) { animation-delay: 0.3s; }
|
||||
.text span:nth-child(5) { animation-delay: 0.4s; }
|
||||
.text span:nth-child(6) { animation-delay: 0.5s; }
|
||||
.text span:nth-child(7) { animation-delay: 0.6s; }
|
||||
.text span:nth-child(8) { animation-delay: 0.7s; }
|
||||
.text span:nth-child(9) { animation-delay: 0.8s; }
|
||||
.text span:nth-child(10) { animation-delay: 0.9s; }
|
||||
.text span:nth-child(11) { animation-delay: 1.0s; }
|
||||
.text span:nth-child(12) { animation-delay: 1.1s; }
|
||||
.text span:nth-child(13) { animation-delay: 1.2s; }
|
||||
.text span:nth-child(14) { animation-delay: 1.3s; }
|
||||
.text span:nth-child(15) { animation-delay: 1.4s; }
|
||||
@@ -1,21 +1,27 @@
|
||||
@page "/"
|
||||
@using Butter.Dtos;
|
||||
@using MilkStream.Components.Layout
|
||||
@using MilkStream.Services
|
||||
|
||||
@inject NavigationManager navigationManager
|
||||
@inject ISessionStorageService sessionStorage
|
||||
@inject LoginService loginService
|
||||
@inject ProtectedSessionStorage sessionStorage
|
||||
@inject ProtectedLocalStorage localStorage
|
||||
|
||||
<PageTitle>Home</PageTitle>
|
||||
|
||||
<div class="home-imagecontainer">
|
||||
<div class="">
|
||||
@if (isLoading) {
|
||||
<LoadSpinner/>
|
||||
} else {
|
||||
@if (mediaList.Count == 0) {
|
||||
<div class="no-media">
|
||||
<h2>No media available</h2>
|
||||
<p>Please log in to see your media.</p>
|
||||
<div class="border-warning border-5 border-opacity-100 rounded-3 p-3 m-5 bg-warning-subtle text-center">
|
||||
<h2 class="text-warning">No media available</h2>
|
||||
@if (loginService.IsLoggedIn) {
|
||||
<p class="text-warning-emphasis">You are logged in, but there is no media available at the moment.</p>
|
||||
} else {
|
||||
<p class="text-warning-emphasis">Please <a href="/login" class="link-warning">log-in</a> to be able to browse any media.</p>
|
||||
}
|
||||
</div>
|
||||
} else {
|
||||
<div class="media-list">
|
||||
@@ -34,10 +40,13 @@
|
||||
List<MediaDto> mediaList = new();
|
||||
|
||||
protected async override Task OnAfterRenderAsync(bool firstRender) {
|
||||
var ssid = await sessionStorage.GetItemAsync<string?>("userId");
|
||||
var authToken = await sessionStorage.GetItemAsync<string?>("authToken");
|
||||
|
||||
if (string.IsNullOrEmpty(ssid) || string.IsNullOrEmpty(authToken)) { isLoading = false; }
|
||||
|
||||
if (firstRender) {
|
||||
loginService.LoggedUserChanged += (sender, dto) => StateHasChanged();
|
||||
//do nothing for now
|
||||
await Task.Delay(1);
|
||||
isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,57 +1,53 @@
|
||||
@page "/login"
|
||||
@using Butter.Dtos
|
||||
@using Microsoft.AspNetCore.Mvc.RazorPages
|
||||
@using MilkStream.Services
|
||||
|
||||
@inject NavigationManager navigationManager
|
||||
@inject NavigationManager navigation
|
||||
@inject LoginService loginService
|
||||
@inject ISessionStorageService sessionStorage
|
||||
@inject ProtectedSessionStorage sessionStorageService
|
||||
@inject ProtectedLocalStorage protectedLocalStorage
|
||||
|
||||
<PageTitle>Login</PageTitle>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="d-flex flex-column mx-sm-auto mx-3">
|
||||
@if (isLoginFailed){
|
||||
<div class="login-failed login-group">
|
||||
<div class="login-failed-text">Login failed. Please try again.</div>
|
||||
<div class="m-2 p-1 border-danger border-2 rounded-3 bg-danger-subtle align-items-center">
|
||||
<div class="text-danger-emphasis m-2">Login failed. Please try again.</div>
|
||||
</div>
|
||||
}
|
||||
<div class="login-group">
|
||||
<label class="login-lbl">Username</label>
|
||||
<input type="text" placeholder="Username" required="required" id="username" @bind="username"/>
|
||||
<div class="d-flex flex-column align-items-center">
|
||||
<input type="text" class="form-control m-2" placeholder="Username" required="required" id="username" @bind="username"/>
|
||||
<input type="password" class="form-control m-2" placeholder="Password" required="required" id="password" @bind="password"/>
|
||||
<div class="align-self-start m-2"><a class="link-primary" href="#">Forgot password?</a></div>
|
||||
<input type="checkbox" class="btn-check m-2" id="remember-me" autocomplete="off" />
|
||||
<div class="d-flex flex-row justify-content-around">
|
||||
<button class="btn btn-primary m-2" type="submit" id="login-button" @onclick="Login_OnClick">Login</button>
|
||||
<button class="btn btn-outline-primary m-2" type="button" id="register-button" @onclick="Register_OnClick">Register</button>
|
||||
</div>
|
||||
<div class="login-group">
|
||||
<label class="login-lbl">Password</label>
|
||||
<input type="password" placeholder="Password" required="required" id="password" @bind="password"/>
|
||||
</div>
|
||||
<div class="login-group">
|
||||
<div style="margin-left: auto"></div>
|
||||
<div class="login-href">Forgot password?</div>
|
||||
</div>
|
||||
<div class="login-group">
|
||||
<button class="login-btn" type="submit" id="login-button" @onclick="Login_OnClick">Login</button>
|
||||
<a href="/register">
|
||||
<button class="login-btn" type="button" id="register-button">Register</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code{
|
||||
string username = string.Empty;
|
||||
string password = string.Empty;
|
||||
bool isLoginFailed = false;
|
||||
bool isLoginFailed;
|
||||
|
||||
private async Task Login_OnClick() {
|
||||
protected override void OnInitialized() {
|
||||
//if (loginService.IsLoggedIn) navigation.NavigateTo("/", true);
|
||||
}
|
||||
|
||||
void Register_OnClick() => navigation.NavigateTo("register");
|
||||
|
||||
async Task Login_OnClick() {
|
||||
isLoginFailed = false; // Reset the login failure string
|
||||
var result = await loginService.Login(username, password);
|
||||
|
||||
if (result.Item1) { // Login successful
|
||||
await sessionStorage.SetItemAsync("userId", result.Item2?.UserId.ToString()!);
|
||||
await sessionStorage.SetItemAsync("authToken", result.Item2?.Token!);
|
||||
await sessionStorage.SetItemAsync("refreshToken", result.Item2?.RefreshToken!);
|
||||
navigationManager.NavigateTo("/"); // Redirect to home page
|
||||
isLoginFailed = false;
|
||||
await protectedLocalStorage.SetAsync("auth", result.Item2!);
|
||||
navigation.NavigateTo("/", true); // Redirect to home page
|
||||
} else {
|
||||
//TODO: Handle login failure
|
||||
//show a message to the user
|
||||
isLoginFailed = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
.login-container{
|
||||
display: flex;
|
||||
flex-direction: column; /* Stack the form groups vertically */
|
||||
min-width: 300px; /* Set a width for the container */
|
||||
max-width: 500px; /* Set a maximum width for the container */
|
||||
margin: 0 auto; /* Center the container */
|
||||
}
|
||||
|
||||
.login-group{
|
||||
display: flex; /* Use flexbox for each form group */
|
||||
align-items: center; /* Center items vertically */
|
||||
justify-content: center;
|
||||
margin-bottom: 8px; /* Space between form groups */
|
||||
padding: 0 2px 0 2px; /* Add some padding */
|
||||
}
|
||||
|
||||
.login-btn{
|
||||
/*display: flex; /* Use flexbox for the button */
|
||||
align-items: center; /* Center items vertically */
|
||||
margin-top: 15px; /* Space above the button */
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
padding: 2px 4px;
|
||||
border: 1px lightgray solid;
|
||||
border-radius: 5px;
|
||||
background-color: rgb(255, 255, 255); /* Light background */
|
||||
color: rgb(5, 39, 103); /* Dark text */
|
||||
}
|
||||
|
||||
.login-btn:hover, .login-btn:focus {
|
||||
cursor: pointer;
|
||||
background-color: rgb(230, 230, 230); /* Slightly darker on hover */
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
transition: ease-in-out;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
.login-href{
|
||||
align-items: flex-start;
|
||||
margin-left: 0;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.login-href:hover{
|
||||
cursor: pointer;
|
||||
color: rgb(174, 18, 213);
|
||||
text-decoration: underline;
|
||||
transition: ease-in-out;
|
||||
transition-duration: 0.2s;
|
||||
}
|
||||
|
||||
label{
|
||||
flex: 1; /* Allow label to take up space */
|
||||
margin-right: 10px; /* Space between label and input */
|
||||
}
|
||||
|
||||
.login-lbl{
|
||||
text-align-last: right;
|
||||
}
|
||||
|
||||
input {
|
||||
flex: 2; /* Allow input to take up more space */
|
||||
padding: 8px; /* Add some padding for better appearance */
|
||||
border-radius: 4px; /* Rounded corners */
|
||||
background-color: rgba(255, 255, 255, 0.8); /* Semi-transparent */
|
||||
color: rgb(5, 39, 103);
|
||||
border: 1px solid rgb(174, 18, 213); /* Match the gradient */
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:hover, input:focus {
|
||||
cursor: pointer;
|
||||
border-color: rgb(5, 39, 103); /* Change border on focus */
|
||||
outline: none;
|
||||
box-shadow: 0 0 5px rgba(174, 18, 213, 0.5);
|
||||
}
|
||||
|
||||
.login-failed {
|
||||
background-color: darkred;
|
||||
border: 1px solid red;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.login-failed-text {
|
||||
color: white;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -2,32 +2,20 @@
|
||||
@using MilkStream.Services
|
||||
|
||||
@inject LoginService loginService
|
||||
@inject NavigationManager Navigation
|
||||
@inject NavigationManager navigation
|
||||
|
||||
<PageTitle>Register</PageTitle>
|
||||
|
||||
<div class="login-container">
|
||||
<div class="login-group">
|
||||
<label class="login-lbl">Username</label>
|
||||
<input type="text" placeholder="Username" required="required" id="username" @bind="username"/>
|
||||
</div>
|
||||
<div class="login-group">
|
||||
<label class="login-lbl">Email</label>
|
||||
<input type="email" placeholder="Email" required="required" id="email" @bind="email"/>
|
||||
</div>
|
||||
<div class="login-group">
|
||||
<label class="login-lbl">Password</label>
|
||||
<input type="password" placeholder="Password" required="required" id="password" @bind="password"/>
|
||||
</div>
|
||||
<div class="login-group">
|
||||
<label class="login-lbl">Confirm Password</label>
|
||||
<input type="password" placeholder="Confirm Password" required="required" id="confirm-password" @bind="confirmPassword"/>
|
||||
</div>
|
||||
<div class="login-group">
|
||||
<a href="/login"><button class="login-btn" type="button" id="login-button">Login</button></a>
|
||||
<button class="login-btn" type="button" id="register-button" @onclick="Register_OnClick">Register</button>
|
||||
</div>
|
||||
<form class="d-flex flex-column mx-sm-auto mx-3 align-items-center">
|
||||
<input type="text" class="form-control my-1" placeholder="Username" required="required" id="username" @bind="username"/>
|
||||
<input type="email" class="form-control my-1" placeholder="Email" required="required" id="email" @bind="email"/>
|
||||
<input type="password" class="form-control my-1" placeholder="Password" required="required" id="password" @bind="password"/>
|
||||
<input type="password" class="form-control my-1" placeholder="Confirm Password" required="required" id="confirm-password" @bind="confirmPassword"/>
|
||||
<div class="d-flex flex-row justify-content-around">
|
||||
<button class="btn btn-primary m-2" type="button" id="register-button" @onclick="Register_OnClick">Register</button>
|
||||
<button class="btn btn-outline-primary m-2" type="button" id="login-button" @onclick="Login_OnClick">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@code {
|
||||
string username = string.Empty;
|
||||
@@ -35,11 +23,13 @@
|
||||
string password = string.Empty;
|
||||
string confirmPassword = string.Empty;
|
||||
|
||||
void Login_OnClick() => navigation.NavigateTo("login");
|
||||
|
||||
async Task Register_OnClick() {
|
||||
var result = await loginService.Register(username, email, password);
|
||||
|
||||
if (result) {
|
||||
Navigation.NavigateTo("login");
|
||||
navigation.NavigateTo("login");
|
||||
}
|
||||
}
|
||||
}
|
||||
105
MilkStream/Components/Pages/Settings.razor
Normal file
105
MilkStream/Components/Pages/Settings.razor
Normal file
@@ -0,0 +1,105 @@
|
||||
@page "/Settings"
|
||||
@using Butter.Dtos.Folder
|
||||
@using Butter.Dtos.Settings
|
||||
@using Butter.Settings
|
||||
@using MilkStream.Components.SettingBoxes
|
||||
@using MilkStream.Services
|
||||
@using System.Linq;
|
||||
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject LoginService LoginService
|
||||
@inject SettingsService SettingsService
|
||||
@inject FoldersService FoldersService
|
||||
|
||||
<PageTitle>Settings</PageTitle>
|
||||
|
||||
<h3>Settings</h3>
|
||||
@if (settings.Any()) {
|
||||
foreach (var option in settings) {
|
||||
switch (option.DisplayType) {
|
||||
// TODO: should implement text and password at some point
|
||||
case DisplayType.Text:
|
||||
<input type="text" id="@option.Name" class="form-control" @bind="@option.Value"/>
|
||||
break;
|
||||
case DisplayType.Password:
|
||||
<input type="password" id="@option.Name" class="form-control" @bind="@option.Value"/>
|
||||
break;
|
||||
case DisplayType.IntNumber:
|
||||
case DisplayType.FloatNumber:
|
||||
<SettingNumber Setting="option"/>
|
||||
break;
|
||||
case DisplayType.Checkbox:
|
||||
// TODO: maybe decide if we ever need it?
|
||||
case DisplayType.Switch:
|
||||
<SettingSwitch Setting="option"/>
|
||||
break;
|
||||
case DisplayType.DateTimePicker:
|
||||
<div class="">DATETIME NOT IMPLEMENTED Value: @option.Value</div>
|
||||
break;
|
||||
case DisplayType.TimePicker:
|
||||
<SettingRange Setting="option"/>
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
<div class="alert alert-danger my-2 my-sm-5 mx-auto" style="max-width: fit-content" role="alert">
|
||||
No settings available.
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<button class="btn btn-outline-primary my-2" @onclick="OnSaveChanges"><i class="bi bi-floppy2 mx-1"></i>Save Changes</button>
|
||||
</div>
|
||||
<hr/>
|
||||
<h3>Folders</h3>
|
||||
@if (folders.Any()) {
|
||||
foreach (var folder in folders) {
|
||||
<FolderBox Folder="folder"/>
|
||||
}
|
||||
} else {
|
||||
<div class="alert alert-danger my-2 my-sm-5 mx-auto" style="max-width: fit-content" role="alert">
|
||||
No folders available.
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<button class="btn btn-outline-primary my-2" @onclick="OnAddFolder"><i class="bi bi-folder-plus mx-1"></i>Add folder</button>
|
||||
</div>
|
||||
|
||||
|
||||
@code {
|
||||
List<SettingDto> settings = new();
|
||||
List<FolderFullDto> folders = new();
|
||||
|
||||
protected override void OnInitialized() {
|
||||
base.OnInitialized();
|
||||
LoginService.LoggedUserChanged += async (_, _) => {
|
||||
await LoadData();
|
||||
StateHasChanged();
|
||||
};
|
||||
}
|
||||
|
||||
protected async override Task OnAfterRenderAsync(bool firstRender) {
|
||||
if (firstRender) {
|
||||
if (LoginService.IsLoggedIn)
|
||||
await LoadData();
|
||||
else
|
||||
LoginService.LoggedUserChanged += async (_, _) => await LoadData();
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
|
||||
async Task LoadData() {
|
||||
settings = await SettingsService.GetAllSettings() ?? new List<SettingDto>();
|
||||
folders = await FoldersService.GetAllFolders() ?? new List<FolderFullDto>();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
void OnSaveChanges() {
|
||||
SettingsService.OnBeginSave();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
void OnAddFolder() {
|
||||
FoldersService.CreateFolder(new FolderCreateDto() { BasePath = "New Folder" });
|
||||
NavigationManager.Refresh(true);
|
||||
}
|
||||
}
|
||||
17
MilkStream/Components/SettingBoxes/SettingBox.razor
Normal file
17
MilkStream/Components/SettingBoxes/SettingBox.razor
Normal file
@@ -0,0 +1,17 @@
|
||||
@using Butter.Dtos.Settings
|
||||
@using MilkStream.Services
|
||||
|
||||
@inject SettingsService SettingsService
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public required SettingDto Setting { get; set; }
|
||||
|
||||
protected virtual SettingDto GetSetting() {
|
||||
return Setting;
|
||||
}
|
||||
|
||||
protected override void OnInitialized() {
|
||||
SettingsService.BeginSave += (sender, args) => SettingsService.UpdateSetting(GetSetting());
|
||||
}
|
||||
}
|
||||
34
MilkStream/Components/SettingBoxes/SettingNumber.razor
Normal file
34
MilkStream/Components/SettingBoxes/SettingNumber.razor
Normal file
@@ -0,0 +1,34 @@
|
||||
@using Butter.Dtos.Settings
|
||||
@using MilkStream.Services
|
||||
@using System.Globalization
|
||||
@inherits SettingBox
|
||||
@inject SettingsService SettingsService
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">@(Setting?.Name ?? "NO NAME")</div>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||
<div>
|
||||
<input type="number" id="@Setting?.Name" class="form-control w-auto" @bind="currentValue" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
|
||||
private float currentValue;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
SettingsService.BeginSave += (sender, args) => SettingsService.UpdateSetting(GetSetting());
|
||||
if (float.TryParse(Setting.Value, out float value)) {
|
||||
currentValue = value;
|
||||
} else {
|
||||
currentValue = 0; // Default value if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
protected override SettingDto GetSetting() {
|
||||
Setting.Value = currentValue.ToString(CultureInfo.InvariantCulture);
|
||||
return Setting;
|
||||
}
|
||||
}
|
||||
32
MilkStream/Components/SettingBoxes/SettingRange.razor
Normal file
32
MilkStream/Components/SettingBoxes/SettingRange.razor
Normal file
@@ -0,0 +1,32 @@
|
||||
@using Butter.Dtos.Settings
|
||||
@using MilkStream.Services
|
||||
@inherits SettingBox
|
||||
@inject SettingsService SettingsService
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">@(Setting?.Name ?? "NO NAME")</div>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||
<div class="d-flex">
|
||||
<div class="mx-2">@currentValue</div>
|
||||
<input type="range" id="@Setting?.Name" class="form-range" @bind="@currentValue"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private int currentValue;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
SettingsService.BeginSave += (sender, args) => SettingsService.UpdateSetting(GetSetting());
|
||||
if (int.TryParse(Setting.Value, out int value)) { currentValue = value; } else {
|
||||
currentValue = 0; // Default value if parsing fails
|
||||
}
|
||||
}
|
||||
|
||||
protected override SettingDto GetSetting() {
|
||||
Setting.Value = currentValue.ToString();
|
||||
return Setting;
|
||||
}
|
||||
|
||||
}
|
||||
31
MilkStream/Components/SettingBoxes/SettingSwitch.razor
Normal file
31
MilkStream/Components/SettingBoxes/SettingSwitch.razor
Normal file
@@ -0,0 +1,31 @@
|
||||
@using Butter.Dtos.Settings
|
||||
@using Butter.Settings
|
||||
@using MilkStream.Services
|
||||
@inherits SettingBox
|
||||
@inject SettingsService SettingsService
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">@(Setting?.Name ?? "NO NAME")</div>
|
||||
<div class="card-body d-flex justify-content-between align-items-center">
|
||||
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
|
||||
<div class="form-switch">
|
||||
<input type="checkbox" id="@Setting?.Name" class="form-check-input m-1" @bind="isChecked"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool isChecked;
|
||||
|
||||
protected override void OnInitialized() {
|
||||
SettingsService.BeginSave += (sender, args) => SettingsService.UpdateSetting(GetSetting());
|
||||
if (bool.TryParse(Setting.Value, out var parsedValue)) {
|
||||
isChecked = parsedValue;
|
||||
}
|
||||
}
|
||||
|
||||
protected override SettingDto GetSetting() {
|
||||
Setting.Value = isChecked.ToString();
|
||||
return Setting;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage
|
||||
@using Microsoft.JSInterop
|
||||
@using MilkStream
|
||||
@using MilkStream.Components
|
||||
@@ -20,4 +20,8 @@ RUN dotnet publish "./MilkStream.csproj" -c $BUILD_CONFIGURATION -o /app/publish
|
||||
FROM base AS final
|
||||
WORKDIR /app
|
||||
COPY --from=publish /app/publish .
|
||||
RUN mkdir -p /home/app/.aspnet/DataProtection-Keys
|
||||
RUN chown -R $APP_UID /home/app/
|
||||
RUN chmod -R 770 /home/app/.aspnet/DataProtection-Keys
|
||||
VOLUME ["/home/app/.aspnet/DataProtection-Keys"]
|
||||
ENTRYPOINT ["dotnet", "MilkStream.dll"]
|
||||
|
||||
38
MilkStream/JwtTokenRefresher.cs
Normal file
38
MilkStream/JwtTokenRefresher.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using MilkStream.Services;
|
||||
using System.Net;
|
||||
using Microsoft.IdentityModel.JsonWebTokens;
|
||||
|
||||
namespace MilkStream;
|
||||
|
||||
/// <summary>
|
||||
/// Can be added to an HTTP client to automatically refresh JWT tokens when they expire.
|
||||
/// </summary>
|
||||
/// <param name="loginService">login service that handles the current identity</param>
|
||||
public class JwtTokenRefresher(LoginService loginService, ILogger<JwtTokenRefresher> logger) : DelegatingHandler {
|
||||
protected async override Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request,
|
||||
CancellationToken cancellationToken
|
||||
) {
|
||||
var jwt = new JsonWebToken(request.Headers.Authorization?.Parameter);
|
||||
// Check if the JWT is valid and not going to expire within 1 minute otherwise send the request as is.
|
||||
if (jwt.ValidTo >= DateTime.UtcNow.AddMinutes(1)) {
|
||||
logger.LogDebug("JWT Token valid up to {JwtValidTo}, no need to refresh.", jwt.ValidTo);
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
logger.LogDebug("JWT Token expired or expiring soon at {JwtValidTo}, refreshing token...", jwt.ValidTo);
|
||||
|
||||
// If the JWT is expired or going to expire within 1 minute, reauthenticate.
|
||||
var auth = await loginService.Reauthenticate();
|
||||
if (auth == null) {
|
||||
// If reauthentication fails, return an unauthorized response.
|
||||
logger.LogDebug("Failed to reauthenticate, returning unauthorized response.");
|
||||
return new HttpResponseMessage(HttpStatusCode.Unauthorized) {
|
||||
Content = new StringContent("Authentication failed. Please log in again.")
|
||||
};
|
||||
}
|
||||
jwt = new JsonWebToken(auth.Token);
|
||||
logger.LogDebug("JWT Token refreshed, valid up to {JwtValidTo}.", jwt.ValidTo);
|
||||
request.Headers.Authorization = new("bearer", auth.Token);
|
||||
return await base.SendAsync(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
3
MilkStream/Lib/.gitignore
vendored
3
MilkStream/Lib/.gitignore
vendored
@@ -1,3 +0,0 @@
|
||||
bootstrap/dist/*
|
||||
bootstrap/js/dist/*
|
||||
bootstrap
|
||||
21
MilkStream/Lib/bootstrap/LICENSE
Normal file
21
MilkStream/Lib/bootstrap/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2011-2025 The Bootstrap Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
247
MilkStream/Lib/bootstrap/README.md
Normal file
247
MilkStream/Lib/bootstrap/README.md
Normal file
@@ -0,0 +1,247 @@
|
||||
<p align="center">
|
||||
<a href="https://getbootstrap.com/">
|
||||
<img src="https://getbootstrap.com/docs/5.3/assets/brand/bootstrap-logo-shadow.png" alt="Bootstrap logo" width="200" height="165">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h3 align="center">Bootstrap</h3>
|
||||
|
||||
<p align="center">
|
||||
Sleek, intuitive, and powerful front-end framework for faster and easier web development.
|
||||
<br>
|
||||
<a href="https://getbootstrap.com/docs/5.3/"><strong>Explore Bootstrap docs »</strong></a>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://github.com/twbs/bootstrap/issues/new?assignees=-&labels=bug&template=bug_report.yml">Report bug</a>
|
||||
·
|
||||
<a href="https://github.com/twbs/bootstrap/issues/new?assignees=&labels=feature&template=feature_request.yml">Request feature</a>
|
||||
·
|
||||
<a href="https://themes.getbootstrap.com/">Themes</a>
|
||||
·
|
||||
<a href="https://blog.getbootstrap.com/">Blog</a>
|
||||
</p>
|
||||
|
||||
|
||||
## Bootstrap 5
|
||||
|
||||
Our default branch is for development of our Bootstrap 5 release. Head to the [`v4-dev` branch](https://github.com/twbs/bootstrap/tree/v4-dev) to view the readme, documentation, and source code for Bootstrap 4.
|
||||
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Quick start](#quick-start)
|
||||
- [Status](#status)
|
||||
- [What’s included](#whats-included)
|
||||
- [Bugs and feature requests](#bugs-and-feature-requests)
|
||||
- [Documentation](#documentation)
|
||||
- [Contributing](#contributing)
|
||||
- [Community](#community)
|
||||
- [Versioning](#versioning)
|
||||
- [Creators](#creators)
|
||||
- [Thanks](#thanks)
|
||||
- [Copyright and license](#copyright-and-license)
|
||||
|
||||
|
||||
## Quick start
|
||||
|
||||
Several quick start options are available:
|
||||
|
||||
- [Download the latest release](https://github.com/twbs/bootstrap/archive/v5.3.7.zip)
|
||||
- Clone the repo: `git clone https://github.com/twbs/bootstrap.git`
|
||||
- Install with [npm](https://www.npmjs.com/): `npm install bootstrap@v5.3.7`
|
||||
- Install with [yarn](https://yarnpkg.com/): `yarn add bootstrap@v5.3.7`
|
||||
- Install with [Bun](https://bun.sh/): `bun add bootstrap@v5.3.7`
|
||||
- Install with [Composer](https://getcomposer.org/): `composer require twbs/bootstrap:5.3.7`
|
||||
- Install with [NuGet](https://www.nuget.org/): CSS: `Install-Package bootstrap` Sass: `Install-Package bootstrap.sass`
|
||||
|
||||
Read the [Getting started page](https://getbootstrap.com/docs/5.3/getting-started/introduction/) for information on the framework contents, templates, examples, and more.
|
||||
|
||||
|
||||
## Status
|
||||
|
||||
[](https://github.com/twbs/bootstrap/actions/workflows/js.yml?query=workflow%3AJS+branch%3Amain)
|
||||
[](https://www.npmjs.com/package/bootstrap)
|
||||
[](https://rubygems.org/gems/bootstrap)
|
||||
[](https://atmospherejs.com/twbs/bootstrap)
|
||||
[](https://packagist.org/packages/twbs/bootstrap)
|
||||
[](https://www.nuget.org/packages/bootstrap/absoluteLatest)
|
||||
[](https://coveralls.io/github/twbs/bootstrap?branch=main)
|
||||
[](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css)
|
||||
[](https://github.com/twbs/bootstrap/blob/main/dist/css/bootstrap.min.css)
|
||||
[](https://github.com/twbs/bootstrap/blob/main/dist/js/bootstrap.min.js)
|
||||
[](https://github.com/twbs/bootstrap/blob/main/dist/js/bootstrap.min.js)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
|
||||
|
||||
## What’s included
|
||||
|
||||
Within the download you’ll find the following directories and files, logically grouping common assets and providing both compiled and minified variations.
|
||||
|
||||
<details>
|
||||
<summary>Download contents</summary>
|
||||
|
||||
```text
|
||||
bootstrap/
|
||||
├── css/
|
||||
│ ├── bootstrap-grid.css
|
||||
│ ├── bootstrap-grid.css.map
|
||||
│ ├── bootstrap-grid.min.css
|
||||
│ ├── bootstrap-grid.min.css.map
|
||||
│ ├── bootstrap-grid.rtl.css
|
||||
│ ├── bootstrap-grid.rtl.css.map
|
||||
│ ├── bootstrap-grid.rtl.min.css
|
||||
│ ├── bootstrap-grid.rtl.min.css.map
|
||||
│ ├── bootstrap-reboot.css
|
||||
│ ├── bootstrap-reboot.css.map
|
||||
│ ├── bootstrap-reboot.min.css
|
||||
│ ├── bootstrap-reboot.min.css.map
|
||||
│ ├── bootstrap-reboot.rtl.css
|
||||
│ ├── bootstrap-reboot.rtl.css.map
|
||||
│ ├── bootstrap-reboot.rtl.min.css
|
||||
│ ├── bootstrap-reboot.rtl.min.css.map
|
||||
│ ├── bootstrap-utilities.css
|
||||
│ ├── bootstrap-utilities.css.map
|
||||
│ ├── bootstrap-utilities.min.css
|
||||
│ ├── bootstrap-utilities.min.css.map
|
||||
│ ├── bootstrap-utilities.rtl.css
|
||||
│ ├── bootstrap-utilities.rtl.css.map
|
||||
│ ├── bootstrap-utilities.rtl.min.css
|
||||
│ ├── bootstrap-utilities.rtl.min.css.map
|
||||
│ ├── bootstrap.css
|
||||
│ ├── bootstrap.css.map
|
||||
│ ├── bootstrap.min.css
|
||||
│ ├── bootstrap.min.css.map
|
||||
│ ├── bootstrap.rtl.css
|
||||
│ ├── bootstrap.rtl.css.map
|
||||
│ ├── bootstrap.rtl.min.css
|
||||
│ └── bootstrap.rtl.min.css.map
|
||||
└── js/
|
||||
├── bootstrap.bundle.js
|
||||
├── bootstrap.bundle.js.map
|
||||
├── bootstrap.bundle.min.js
|
||||
├── bootstrap.bundle.min.js.map
|
||||
├── bootstrap.esm.js
|
||||
├── bootstrap.esm.js.map
|
||||
├── bootstrap.esm.min.js
|
||||
├── bootstrap.esm.min.js.map
|
||||
├── bootstrap.js
|
||||
├── bootstrap.js.map
|
||||
├── bootstrap.min.js
|
||||
└── bootstrap.min.js.map
|
||||
```
|
||||
</details>
|
||||
|
||||
We provide compiled CSS and JS (`bootstrap.*`), as well as compiled and minified CSS and JS (`bootstrap.min.*`). [Source maps](https://web.dev/articles/source-maps) (`bootstrap.*.map`) are available for use with certain browsers’ developer tools. Bundled JS files (`bootstrap.bundle.js` and minified `bootstrap.bundle.min.js`) include [Popper](https://popper.js.org/docs/v2/).
|
||||
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
Have a bug or a feature request? Please first read the [issue guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md#using-the-issue-tracker) and search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/twbs/bootstrap/issues/new/choose).
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
Bootstrap’s documentation, included in this repo in the root directory, is built with [Astro](https://astro.build/) and publicly hosted on GitHub Pages at <https://getbootstrap.com/>. The docs may also be run locally.
|
||||
|
||||
Documentation search is powered by [Algolia's DocSearch](https://docsearch.algolia.com/).
|
||||
|
||||
### Running documentation locally
|
||||
|
||||
1. Run `npm install` to install the Node.js dependencies, including Astro (the site builder).
|
||||
2. Run `npm run test` (or a specific npm script) to rebuild distributed CSS and JavaScript files, as well as our docs assets.
|
||||
3. From the root `/bootstrap` directory, run `npm run docs-serve` in the command line.
|
||||
4. Open `http://localhost:9001/` in your browser, and voilà.
|
||||
|
||||
Learn more about using Astro by reading its [documentation](https://docs.astro.build/en/getting-started/).
|
||||
|
||||
### Documentation for previous releases
|
||||
|
||||
You can find all our previous releases docs on <https://getbootstrap.com/docs/versions/>.
|
||||
|
||||
[Previous releases](https://github.com/twbs/bootstrap/releases) and their documentation are also available for download.
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
Please read through our [contributing guidelines](https://github.com/twbs/bootstrap/blob/main/.github/CONTRIBUTING.md). Included are directions for opening issues, coding standards, and notes on development.
|
||||
|
||||
Moreover, if your pull request contains JavaScript patches or features, you must include [relevant unit tests](https://github.com/twbs/bootstrap/tree/main/js/tests). All HTML and CSS should conform to the [Code Guide](https://github.com/mdo/code-guide), maintained by [Mark Otto](https://github.com/mdo).
|
||||
|
||||
Editor preferences are available in the [editor config](https://github.com/twbs/bootstrap/blob/main/.editorconfig) for easy use in common text editors. Read more and download plugins at <https://editorconfig.org/>.
|
||||
|
||||
|
||||
## Community
|
||||
|
||||
Get updates on Bootstrap’s development and chat with the project maintainers and community members.
|
||||
|
||||
- Follow [@getbootstrap on X](https://x.com/getbootstrap).
|
||||
- Read and subscribe to [The Official Bootstrap Blog](https://blog.getbootstrap.com/).
|
||||
- Ask questions and explore [our GitHub Discussions](https://github.com/twbs/bootstrap/discussions).
|
||||
- Discuss, ask questions, and more on [the community Discord](https://discord.gg/bZUvakRU3M) or [Bootstrap subreddit](https://www.reddit.com/r/bootstrap/).
|
||||
- Chat with fellow Bootstrappers in IRC. On the `irc.libera.chat` server, in the `#bootstrap` channel.
|
||||
- Implementation help may be found at Stack Overflow (tagged [`bootstrap-5`](https://stackoverflow.com/questions/tagged/bootstrap-5)).
|
||||
- Developers should use the keyword `bootstrap` on packages which modify or add to the functionality of Bootstrap when distributing through [npm](https://www.npmjs.com/browse/keyword/bootstrap) or similar delivery mechanisms for maximum discoverability.
|
||||
|
||||
|
||||
## Versioning
|
||||
|
||||
For transparency into our release cycle and in striving to maintain backward compatibility, Bootstrap is maintained under [the Semantic Versioning guidelines](https://semver.org/). Sometimes we screw up, but we adhere to those rules whenever possible.
|
||||
|
||||
See [the Releases section of our GitHub project](https://github.com/twbs/bootstrap/releases) for changelogs for each release version of Bootstrap. Release announcement posts on [the official Bootstrap blog](https://blog.getbootstrap.com/) contain summaries of the most noteworthy changes made in each release.
|
||||
|
||||
|
||||
## Creators
|
||||
|
||||
**Mark Otto**
|
||||
|
||||
- <https://x.com/mdo>
|
||||
- <https://github.com/mdo>
|
||||
|
||||
**Jacob Thornton**
|
||||
|
||||
- <https://x.com/fat>
|
||||
- <https://github.com/fat>
|
||||
|
||||
|
||||
## Thanks
|
||||
|
||||
<a href="https://www.browserstack.com/">
|
||||
<img src="https://live.browserstack.com/images/opensource/browserstack-logo.svg" alt="BrowserStack" width="192" height="42">
|
||||
</a>
|
||||
|
||||
Thanks to [BrowserStack](https://www.browserstack.com/) for providing the infrastructure that allows us to test in real browsers!
|
||||
|
||||
<a href="https://www.netlify.com/">
|
||||
<img src="https://www.netlify.com/v3/img/components/full-logo-light.svg" alt="Netlify" width="147" height="40">
|
||||
</a>
|
||||
|
||||
Thanks to [Netlify](https://www.netlify.com/) for providing us with Deploy Previews!
|
||||
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/bootstrap#sponsor)]
|
||||
|
||||
[](https://opencollective.com/bootstrap/sponsor/0/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/1/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/2/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/3/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/4/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/5/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/6/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/7/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/8/website)
|
||||
[](https://opencollective.com/bootstrap/sponsor/9/website)
|
||||
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/bootstrap#backer)]
|
||||
|
||||
[](https://opencollective.com/bootstrap#backers)
|
||||
|
||||
|
||||
## Copyright and license
|
||||
|
||||
Code and documentation copyright 2011-2025 the [Bootstrap Authors](https://github.com/twbs/bootstrap/graphs/contributors). Code released under the [MIT License](https://github.com/twbs/bootstrap/blob/main/LICENSE). Docs released under [Creative Commons](https://creativecommons.org/licenses/by/3.0/).
|
||||
4085
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
4085
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4084
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
4084
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-grid.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
597
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
597
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.css
vendored
Normal file
@@ -0,0 +1,597 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.7 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
||||
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
594
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
594
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.rtl.css
vendored
Normal file
@@ -0,0 +1,594 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.3.7 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2025 The Bootstrap Authors
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
*/
|
||||
:root,
|
||||
[data-bs-theme=light] {
|
||||
--bs-blue: #0d6efd;
|
||||
--bs-indigo: #6610f2;
|
||||
--bs-purple: #6f42c1;
|
||||
--bs-pink: #d63384;
|
||||
--bs-red: #dc3545;
|
||||
--bs-orange: #fd7e14;
|
||||
--bs-yellow: #ffc107;
|
||||
--bs-green: #198754;
|
||||
--bs-teal: #20c997;
|
||||
--bs-cyan: #0dcaf0;
|
||||
--bs-black: #000;
|
||||
--bs-white: #fff;
|
||||
--bs-gray: #6c757d;
|
||||
--bs-gray-dark: #343a40;
|
||||
--bs-gray-100: #f8f9fa;
|
||||
--bs-gray-200: #e9ecef;
|
||||
--bs-gray-300: #dee2e6;
|
||||
--bs-gray-400: #ced4da;
|
||||
--bs-gray-500: #adb5bd;
|
||||
--bs-gray-600: #6c757d;
|
||||
--bs-gray-700: #495057;
|
||||
--bs-gray-800: #343a40;
|
||||
--bs-gray-900: #212529;
|
||||
--bs-primary: #0d6efd;
|
||||
--bs-secondary: #6c757d;
|
||||
--bs-success: #198754;
|
||||
--bs-info: #0dcaf0;
|
||||
--bs-warning: #ffc107;
|
||||
--bs-danger: #dc3545;
|
||||
--bs-light: #f8f9fa;
|
||||
--bs-dark: #212529;
|
||||
--bs-primary-rgb: 13, 110, 253;
|
||||
--bs-secondary-rgb: 108, 117, 125;
|
||||
--bs-success-rgb: 25, 135, 84;
|
||||
--bs-info-rgb: 13, 202, 240;
|
||||
--bs-warning-rgb: 255, 193, 7;
|
||||
--bs-danger-rgb: 220, 53, 69;
|
||||
--bs-light-rgb: 248, 249, 250;
|
||||
--bs-dark-rgb: 33, 37, 41;
|
||||
--bs-primary-text-emphasis: #052c65;
|
||||
--bs-secondary-text-emphasis: #2b2f32;
|
||||
--bs-success-text-emphasis: #0a3622;
|
||||
--bs-info-text-emphasis: #055160;
|
||||
--bs-warning-text-emphasis: #664d03;
|
||||
--bs-danger-text-emphasis: #58151c;
|
||||
--bs-light-text-emphasis: #495057;
|
||||
--bs-dark-text-emphasis: #495057;
|
||||
--bs-primary-bg-subtle: #cfe2ff;
|
||||
--bs-secondary-bg-subtle: #e2e3e5;
|
||||
--bs-success-bg-subtle: #d1e7dd;
|
||||
--bs-info-bg-subtle: #cff4fc;
|
||||
--bs-warning-bg-subtle: #fff3cd;
|
||||
--bs-danger-bg-subtle: #f8d7da;
|
||||
--bs-light-bg-subtle: #fcfcfd;
|
||||
--bs-dark-bg-subtle: #ced4da;
|
||||
--bs-primary-border-subtle: #9ec5fe;
|
||||
--bs-secondary-border-subtle: #c4c8cb;
|
||||
--bs-success-border-subtle: #a3cfbb;
|
||||
--bs-info-border-subtle: #9eeaf9;
|
||||
--bs-warning-border-subtle: #ffe69c;
|
||||
--bs-danger-border-subtle: #f1aeb5;
|
||||
--bs-light-border-subtle: #e9ecef;
|
||||
--bs-dark-border-subtle: #adb5bd;
|
||||
--bs-white-rgb: 255, 255, 255;
|
||||
--bs-black-rgb: 0, 0, 0;
|
||||
--bs-font-sans-serif: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
--bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--bs-gradient: linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));
|
||||
--bs-body-font-family: var(--bs-font-sans-serif);
|
||||
--bs-body-font-size: 1rem;
|
||||
--bs-body-font-weight: 400;
|
||||
--bs-body-line-height: 1.5;
|
||||
--bs-body-color: #212529;
|
||||
--bs-body-color-rgb: 33, 37, 41;
|
||||
--bs-body-bg: #fff;
|
||||
--bs-body-bg-rgb: 255, 255, 255;
|
||||
--bs-emphasis-color: #000;
|
||||
--bs-emphasis-color-rgb: 0, 0, 0;
|
||||
--bs-secondary-color: rgba(33, 37, 41, 0.75);
|
||||
--bs-secondary-color-rgb: 33, 37, 41;
|
||||
--bs-secondary-bg: #e9ecef;
|
||||
--bs-secondary-bg-rgb: 233, 236, 239;
|
||||
--bs-tertiary-color: rgba(33, 37, 41, 0.5);
|
||||
--bs-tertiary-color-rgb: 33, 37, 41;
|
||||
--bs-tertiary-bg: #f8f9fa;
|
||||
--bs-tertiary-bg-rgb: 248, 249, 250;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #0d6efd;
|
||||
--bs-link-color-rgb: 13, 110, 253;
|
||||
--bs-link-decoration: underline;
|
||||
--bs-link-hover-color: #0a58ca;
|
||||
--bs-link-hover-color-rgb: 10, 88, 202;
|
||||
--bs-code-color: #d63384;
|
||||
--bs-highlight-color: #212529;
|
||||
--bs-highlight-bg: #fff3cd;
|
||||
--bs-border-width: 1px;
|
||||
--bs-border-style: solid;
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-color-translucent: rgba(0, 0, 0, 0.175);
|
||||
--bs-border-radius: 0.375rem;
|
||||
--bs-border-radius-sm: 0.25rem;
|
||||
--bs-border-radius-lg: 0.5rem;
|
||||
--bs-border-radius-xl: 1rem;
|
||||
--bs-border-radius-xxl: 2rem;
|
||||
--bs-border-radius-2xl: var(--bs-border-radius-xxl);
|
||||
--bs-border-radius-pill: 50rem;
|
||||
--bs-box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
--bs-box-shadow-sm: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
||||
--bs-box-shadow-lg: 0 1rem 3rem rgba(0, 0, 0, 0.175);
|
||||
--bs-box-shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.075);
|
||||
--bs-focus-ring-width: 0.25rem;
|
||||
--bs-focus-ring-opacity: 0.25;
|
||||
--bs-focus-ring-color: rgba(13, 110, 253, 0.25);
|
||||
--bs-form-valid-color: #198754;
|
||||
--bs-form-valid-border-color: #198754;
|
||||
--bs-form-invalid-color: #dc3545;
|
||||
--bs-form-invalid-border-color: #dc3545;
|
||||
}
|
||||
|
||||
[data-bs-theme=dark] {
|
||||
color-scheme: dark;
|
||||
--bs-body-color: #dee2e6;
|
||||
--bs-body-color-rgb: 222, 226, 230;
|
||||
--bs-body-bg: #212529;
|
||||
--bs-body-bg-rgb: 33, 37, 41;
|
||||
--bs-emphasis-color: #fff;
|
||||
--bs-emphasis-color-rgb: 255, 255, 255;
|
||||
--bs-secondary-color: rgba(222, 226, 230, 0.75);
|
||||
--bs-secondary-color-rgb: 222, 226, 230;
|
||||
--bs-secondary-bg: #343a40;
|
||||
--bs-secondary-bg-rgb: 52, 58, 64;
|
||||
--bs-tertiary-color: rgba(222, 226, 230, 0.5);
|
||||
--bs-tertiary-color-rgb: 222, 226, 230;
|
||||
--bs-tertiary-bg: #2b3035;
|
||||
--bs-tertiary-bg-rgb: 43, 48, 53;
|
||||
--bs-primary-text-emphasis: #6ea8fe;
|
||||
--bs-secondary-text-emphasis: #a7acb1;
|
||||
--bs-success-text-emphasis: #75b798;
|
||||
--bs-info-text-emphasis: #6edff6;
|
||||
--bs-warning-text-emphasis: #ffda6a;
|
||||
--bs-danger-text-emphasis: #ea868f;
|
||||
--bs-light-text-emphasis: #f8f9fa;
|
||||
--bs-dark-text-emphasis: #dee2e6;
|
||||
--bs-primary-bg-subtle: #031633;
|
||||
--bs-secondary-bg-subtle: #161719;
|
||||
--bs-success-bg-subtle: #051b11;
|
||||
--bs-info-bg-subtle: #032830;
|
||||
--bs-warning-bg-subtle: #332701;
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-light-bg-subtle: #343a40;
|
||||
--bs-dark-bg-subtle: #1a1d20;
|
||||
--bs-primary-border-subtle: #084298;
|
||||
--bs-secondary-border-subtle: #41464b;
|
||||
--bs-success-border-subtle: #0f5132;
|
||||
--bs-info-border-subtle: #087990;
|
||||
--bs-warning-border-subtle: #997404;
|
||||
--bs-danger-border-subtle: #842029;
|
||||
--bs-light-border-subtle: #495057;
|
||||
--bs-dark-border-subtle: #343a40;
|
||||
--bs-heading-color: inherit;
|
||||
--bs-link-color: #6ea8fe;
|
||||
--bs-link-hover-color: #8bb9fe;
|
||||
--bs-link-color-rgb: 110, 168, 254;
|
||||
--bs-link-hover-color-rgb: 139, 185, 254;
|
||||
--bs-code-color: #e685b5;
|
||||
--bs-highlight-color: #dee2e6;
|
||||
--bs-highlight-bg: #664d03;
|
||||
--bs-border-color: #495057;
|
||||
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
|
||||
--bs-form-valid-color: #75b798;
|
||||
--bs-form-valid-border-color: #75b798;
|
||||
--bs-form-invalid-color: #ea868f;
|
||||
--bs-form-invalid-border-color: #ea868f;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--bs-body-font-family);
|
||||
font-size: var(--bs-body-font-size);
|
||||
font-weight: var(--bs-body-font-weight);
|
||||
line-height: var(--bs-body-line-height);
|
||||
color: var(--bs-body-color);
|
||||
text-align: var(--bs-body-text-align);
|
||||
background-color: var(--bs-body-bg);
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
border: 0;
|
||||
border-top: var(--bs-border-width) solid;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
color: var(--bs-heading-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1875em;
|
||||
color: var(--bs-highlight-color);
|
||||
background-color: var(--bs-highlight-bg);
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgba(var(--bs-link-color-rgb), var(--bs-link-opacity, 1));
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
--bs-link-color-rgb: var(--bs-link-hover-color-rgb);
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: var(--bs-font-monospace);
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-code-color);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.1875rem 0.375rem;
|
||||
font-size: 0.875em;
|
||||
color: var(--bs-body-bg);
|
||||
background-color: var(--bs-body-color);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: var(--bs-secondary-color);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: inherit;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
-webkit-appearance: textfield;
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
||||
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5406
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
5406
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
5397
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
5397
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
6
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap-utilities.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12043
MilkStream/Lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
12043
MilkStream/Lib/bootstrap/dist/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
MilkStream/Lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
MilkStream/Lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
6
MilkStream/Lib/bootstrap/dist/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
MilkStream/Lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12016
MilkStream/Lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
12016
MilkStream/Lib/bootstrap/dist/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
MilkStream/Lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap.rtl.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6
MilkStream/Lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
6
MilkStream/Lib/bootstrap/dist/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
MilkStream/Lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/css/bootstrap.rtl.min.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
6315
MilkStream/Lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
6315
MilkStream/Lib/bootstrap/dist/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
MilkStream/Lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
1
MilkStream/Lib/bootstrap/dist/js/bootstrap.bundle.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
MilkStream/Lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
7
MilkStream/Lib/bootstrap/dist/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user