2026-05-14 12:31:35 +02:00
|
|
|
|
using AuthorBuddy.Web.Data;
|
|
|
|
|
|
using AuthorBuddy.Web.Models;
|
2026-05-20 06:12:54 +02:00
|
|
|
|
using LiteDB;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
|
|
|
|
|
namespace AuthorBuddy.Web.Services;
|
|
|
|
|
|
|
|
|
|
|
|
public class ProjectService : IProjectService
|
|
|
|
|
|
{
|
2026-05-20 06:12:54 +02:00
|
|
|
|
private readonly ILiteDatabase _db;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
private readonly IFileScannerService _fileScannerService;
|
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
|
public ProjectService(ILiteDatabase db, IFileScannerService fileScannerService)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-05-20 06:12:54 +02:00
|
|
|
|
_db = db;
|
2026-05-14 12:31:35 +02:00
|
|
|
|
_fileScannerService = fileScannerService;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
private ILiteCollection<FileCategoryEntity> CategoryCol =>
|
|
|
|
|
|
_db.GetCollection<FileCategoryEntity>("fileCategories");
|
|
|
|
|
|
|
|
|
|
|
|
public Task<int> CreateNewProjectAsync(string projectName, string rootPath, int userId, ProjectTemplate template = ProjectTemplate.Novel)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
|
if (template == ProjectTemplate.Technical)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
|
string[] subFolders = { "00_Anforderungen", "01_UseCases", "02_Architektur", "03_Glossar" };
|
|
|
|
|
|
foreach (var subFolder in subFolders)
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(Path.Combine(rootPath, subFolder));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string readmePath = Path.Combine(rootPath, "00_Projekt_Info.md");
|
|
|
|
|
|
File.WriteAllText(readmePath, $"# Projekt: {projectName}\nErstellt am: {DateTime.Now}\nTyp: Technische Vorlage (Requirements)");
|
|
|
|
|
|
|
|
|
|
|
|
string lastenheftPath = Path.Combine(rootPath, "Lastenheft.md");
|
|
|
|
|
|
File.WriteAllText(lastenheftPath, $"# {projectName} – Lastenheft\n\n" +
|
|
|
|
|
|
"## 1. Zielbestimmung\n\n" +
|
|
|
|
|
|
"## 2. Produkteinsatz\n\n" +
|
|
|
|
|
|
"## 3. Produktumgebung\n\n" +
|
|
|
|
|
|
"## 4. Funktionale Anforderungen\n\n" +
|
|
|
|
|
|
"## 5. Nicht-funktionale Anforderungen\n\n" +
|
|
|
|
|
|
"## 6. Lieferumfang\n\n");
|
|
|
|
|
|
|
|
|
|
|
|
string pflichtenheftPath = Path.Combine(rootPath, "Pflichtenheft.md");
|
|
|
|
|
|
File.WriteAllText(pflichtenheftPath, $"# {projectName} – Pflichtenheft\n\n" +
|
|
|
|
|
|
"## 1. Systemarchitektur\n\n" +
|
|
|
|
|
|
"## 2. Komponentenbeschreibung\n\n" +
|
|
|
|
|
|
"## 3. Schnittstellendefinition\n\n" +
|
|
|
|
|
|
"## 4. Datenmodell\n\n" +
|
|
|
|
|
|
"## 5. Technische Umsetzung\n\n");
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
2026-05-31 19:31:27 +02:00
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
string[] subFolders = { "00_Drafts", "01_Fakten", "02_Characters", "03_Archive" };
|
|
|
|
|
|
foreach (var subFolder in subFolders)
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(Path.Combine(rootPath, subFolder));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
string readmePath = Path.Combine(rootPath, "00_Projekt_Info.md");
|
|
|
|
|
|
File.WriteAllText(readmePath, $"# Projekt: {projectName}\nErstellt am: {DateTime.Now}");
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
string exposeePath = Path.Combine(rootPath, "Die Handlung (Das Exposé).md");
|
|
|
|
|
|
File.WriteAllText(exposeePath, $"# {projectName} – Die Handlung (Das Exposé)\n\n" +
|
|
|
|
|
|
"## Überblick\n\n" +
|
|
|
|
|
|
"## Erster Akt (Aufbau)\n\n" +
|
|
|
|
|
|
"## Zweiter Akt (Konfrontation)\n\n" +
|
|
|
|
|
|
"## Dritter Akt (Auflösung)\n\n" +
|
|
|
|
|
|
"## Wichtige Wendepunkte\n\n" +
|
|
|
|
|
|
"## Charakterbögen\n\n");
|
|
|
|
|
|
|
|
|
|
|
|
string ideasPath = Path.Combine(rootPath, "Ideen Sammlung.md");
|
|
|
|
|
|
File.WriteAllText(ideasPath, $"# Ideen Sammlung – {projectName}\n\n");
|
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
|
var col = _db.GetCollection<ProjectEntity>("projects");
|
2026-05-14 12:31:35 +02:00
|
|
|
|
var entity = new ProjectEntity
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = projectName,
|
|
|
|
|
|
RootPath = rootPath,
|
2026-05-31 19:31:27 +02:00
|
|
|
|
UserId = userId,
|
|
|
|
|
|
LastOpened = DateTime.Now,
|
|
|
|
|
|
Template = template
|
2026-05-14 12:31:35 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-20 06:12:54 +02:00
|
|
|
|
col.Insert(entity);
|
|
|
|
|
|
return Task.FromResult(entity.Id);
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
public Task<List<Project>> GetAllProjectsAsync(int? userId = null)
|
2026-05-14 12:31:35 +02:00
|
|
|
|
{
|
2026-05-20 06:12:54 +02:00
|
|
|
|
var col = _db.GetCollection<ProjectEntity>("projects");
|
2026-05-31 19:31:27 +02:00
|
|
|
|
var query = userId == null
|
|
|
|
|
|
? col.FindAll()
|
|
|
|
|
|
: col.Find(p => p.UserId == userId);
|
|
|
|
|
|
var list = query
|
2026-05-14 12:31:35 +02:00
|
|
|
|
.OrderByDescending(p => p.LastOpened)
|
|
|
|
|
|
.Select(p => new Project
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = p.Id,
|
|
|
|
|
|
Name = p.Name,
|
2026-05-31 19:31:27 +02:00
|
|
|
|
RootPath = p.RootPath,
|
|
|
|
|
|
Template = p.Template
|
2026-05-14 12:31:35 +02:00
|
|
|
|
})
|
2026-05-20 06:12:54 +02:00
|
|
|
|
.ToList();
|
|
|
|
|
|
return Task.FromResult(list);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task<Project?> GetProjectAsync(int projectId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var col = _db.GetCollection<ProjectEntity>("projects");
|
|
|
|
|
|
var entity = col.FindById(projectId);
|
|
|
|
|
|
if (entity == null) return Task.FromResult<Project?>(null);
|
|
|
|
|
|
return Task.FromResult<Project?>(new Project
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = entity.Id,
|
|
|
|
|
|
Name = entity.Name,
|
2026-05-31 19:31:27 +02:00
|
|
|
|
RootPath = entity.RootPath,
|
|
|
|
|
|
Template = entity.Template
|
2026-05-20 06:12:54 +02:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
public async Task<List<FileTreeItem>> GetProjectFileTreeAsync(int projectId)
|
2026-05-20 06:12:54 +02:00
|
|
|
|
{
|
|
|
|
|
|
var col = _db.GetCollection<ProjectEntity>("projects");
|
|
|
|
|
|
var entity = col.FindById(projectId);
|
|
|
|
|
|
if (entity == null || !Directory.Exists(entity.RootPath))
|
2026-05-31 19:31:27 +02:00
|
|
|
|
return new List<FileTreeItem>();
|
2026-05-20 06:12:54 +02:00
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
var categories = await GetFileCategoriesAsync(projectId);
|
2026-05-20 06:12:54 +02:00
|
|
|
|
var result = new List<FileTreeItem>();
|
2026-05-31 19:31:27 +02:00
|
|
|
|
BuildTree(entity.RootPath, result, 0, categories);
|
|
|
|
|
|
return result;
|
2026-05-20 06:12:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-31 19:31:27 +02:00
|
|
|
|
private static void BuildTree(string directory, List<FileTreeItem> items, int depth, Dictionary<string, FileCategory> categories)
|
2026-05-20 06:12:54 +02:00
|
|
|
|
{
|
|
|
|
|
|
if (depth > 4) return;
|
|
|
|
|
|
|
|
|
|
|
|
var dirInfo = new DirectoryInfo(directory);
|
|
|
|
|
|
if (!dirInfo.Exists) return;
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var dir in dirInfo.GetDirectories().OrderBy(d => d.Name))
|
|
|
|
|
|
{
|
|
|
|
|
|
var node = new FileTreeItem
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = dir.Name,
|
|
|
|
|
|
FullPath = dir.FullName,
|
|
|
|
|
|
IsDirectory = true
|
|
|
|
|
|
};
|
2026-05-31 19:31:27 +02:00
|
|
|
|
BuildTree(dir.FullName, node.Children, depth + 1, categories);
|
2026-05-20 06:12:54 +02:00
|
|
|
|
if (node.Children.Count > 0 || dir.Name.StartsWith("00_") || dir.Name.StartsWith("01_") || dir.Name.StartsWith("02_"))
|
|
|
|
|
|
items.Add(node);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
foreach (var file in dirInfo.GetFiles("*.md").OrderBy(f => f.Name))
|
|
|
|
|
|
{
|
2026-05-31 19:31:27 +02:00
|
|
|
|
categories.TryGetValue(file.FullName, out var cat);
|
2026-05-20 06:12:54 +02:00
|
|
|
|
items.Add(new FileTreeItem
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = file.Name,
|
|
|
|
|
|
FullPath = file.FullName,
|
2026-05-31 19:31:27 +02:00
|
|
|
|
IsDirectory = false,
|
|
|
|
|
|
Category = cat
|
2026-05-20 06:12:54 +02:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public async Task SyncProjectFilesAsync(int projectId, string directoryPath, IProgress<string> progress)
|
|
|
|
|
|
{
|
|
|
|
|
|
await _fileScannerService.ScanProjectFolderAsync(projectId, directoryPath, progress);
|
|
|
|
|
|
}
|
2026-05-25 06:38:16 +02:00
|
|
|
|
|
|
|
|
|
|
public Task<int> CopyProjectAsync(int sourceProjectId, string newName, string newRootPath, int userId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var col = _db.GetCollection<ProjectEntity>("projects");
|
|
|
|
|
|
var source = col.FindById(sourceProjectId);
|
|
|
|
|
|
if (source == null)
|
|
|
|
|
|
throw new InvalidOperationException("Source project not found.");
|
|
|
|
|
|
|
|
|
|
|
|
Directory.CreateDirectory(newRootPath);
|
|
|
|
|
|
CopyDirectory(source.RootPath, newRootPath);
|
|
|
|
|
|
|
|
|
|
|
|
var entity = new ProjectEntity
|
|
|
|
|
|
{
|
|
|
|
|
|
Name = newName,
|
|
|
|
|
|
RootPath = newRootPath,
|
2026-05-31 19:31:27 +02:00
|
|
|
|
UserId = userId,
|
|
|
|
|
|
LastOpened = DateTime.Now,
|
|
|
|
|
|
Template = source.Template
|
2026-05-25 06:38:16 +02:00
|
|
|
|
};
|
|
|
|
|
|
col.Insert(entity);
|
|
|
|
|
|
|
|
|
|
|
|
var settings = _db.GetCollection<SettingEntity>("settings");
|
|
|
|
|
|
var projectSettings = settings.Find(s =>
|
|
|
|
|
|
s.UserId == userId && s.ProjectId == sourceProjectId);
|
|
|
|
|
|
foreach (var s in projectSettings)
|
|
|
|
|
|
{
|
|
|
|
|
|
settings.Insert(new SettingEntity
|
|
|
|
|
|
{
|
|
|
|
|
|
Key = s.Key,
|
|
|
|
|
|
Value = s.Value,
|
|
|
|
|
|
UserId = userId,
|
|
|
|
|
|
ProjectId = entity.Id
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return Task.FromResult(entity.Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void CopyDirectory(string sourceDir, string destDir)
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var dir in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(dir.Replace(sourceDir, destDir));
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach (var file in Directory.GetFiles(sourceDir, "*.*", SearchOption.AllDirectories))
|
|
|
|
|
|
{
|
|
|
|
|
|
var dest = file.Replace(sourceDir, destDir);
|
|
|
|
|
|
File.Copy(file, dest, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-05-31 19:31:27 +02:00
|
|
|
|
|
|
|
|
|
|
public Task<Dictionary<string, FileCategory>> GetFileCategoriesAsync(int projectId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var entries = CategoryCol.Find(e => e.ProjectId == projectId).ToList();
|
|
|
|
|
|
var result = new Dictionary<string, FileCategory>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
foreach (var e in entries)
|
|
|
|
|
|
result[e.FilePath] = e.Category;
|
|
|
|
|
|
return Task.FromResult(result);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task<bool> SetFileCategoryAsync(int projectId, string filePath, FileCategory category)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!FileCategoryHelper.IsValid(category)) return Task.FromResult(false);
|
|
|
|
|
|
|
|
|
|
|
|
if (category.HasFlag(FileCategory.Exposee))
|
|
|
|
|
|
{
|
|
|
|
|
|
var existingExposee = CategoryCol.Find(e => e.ProjectId == projectId)
|
|
|
|
|
|
.FirstOrDefault(e => e.Category.HasFlag(FileCategory.Exposee) && e.FilePath != filePath);
|
|
|
|
|
|
if (existingExposee != null) return Task.FromResult(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var col = CategoryCol;
|
|
|
|
|
|
var existing = col.FindOne(e => e.ProjectId == projectId && e.FilePath == filePath);
|
|
|
|
|
|
if (existing != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (category == FileCategory.None)
|
|
|
|
|
|
col.Delete(existing.Id);
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
existing.Category = category;
|
|
|
|
|
|
col.Update(existing);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (category != FileCategory.None)
|
|
|
|
|
|
{
|
|
|
|
|
|
col.Insert(new FileCategoryEntity
|
|
|
|
|
|
{
|
|
|
|
|
|
ProjectId = projectId,
|
|
|
|
|
|
FilePath = filePath,
|
|
|
|
|
|
Category = category
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
return Task.FromResult(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public Task ClearFileCategoryAsync(int projectId, string filePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
var col = CategoryCol;
|
|
|
|
|
|
var existing = col.FindOne(e => e.ProjectId == projectId && e.FilePath == filePath);
|
|
|
|
|
|
if (existing != null)
|
|
|
|
|
|
col.Delete(existing.Id);
|
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
}
|
2026-05-14 12:31:35 +02:00
|
|
|
|
}
|