feat: MAUI nativo. Aplicativo rodando local no windows.
This commit is contained in:
parent
8b75f1f62d
commit
c5a3b14449
7
.claude/settings.local.json
Normal file
7
.claude/settings.local.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(dotnet:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,73 +1,33 @@
|
|||||||
# Debug Groq Response
|
# A Origem da Alimentação no Brasil e a Importância da Química na Vida Cotidiana (OTHER)
|
||||||
|
|
||||||
URL: https://youtu.be/-EaO2S4Lyvk?si=tJrVNLZCRT2unmD1
|
Source: Por que ARROZ e nÒo BATATA? | Cortes do Manual do Mundo
|
||||||
|
|
||||||
## Raw JSON
|
## Raw JSON
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"category": "OTHER",
|
||||||
|
"documentTitle": "A Origem da Alimentação no Brasil e a Importância da Química na Vida Cotidiana",
|
||||||
"sections": [
|
"sections": [
|
||||||
{
|
{
|
||||||
"title": "Passo 1: Introdução",
|
"title": "Introdução à Alimentação no Brasil",
|
||||||
"content": "Bem-vindo ao tutorial de The Legend of Zelda: Breath of the Wild. Neste vídeo, vamos explorar 9 dicas para iniciantes para ajudá-los a superar as ameaças iniciais do jogo. [SCREENSHOT: 00:00:20]"
|
"content": "A base alimentar brasileira é composta pelo arroz, mas por que não pela batata, se fomos colonizados por europeus que comem batata? Essa é uma pergunta interessante que leva a uma história complexa. Os indígenas no Brasil já cultivavam alguns tipos de arroz, assim como as populações africanas. O arroz é um alimento básico da dieta de 2,5 bilhões de pessoas, aproximadamente 1/3 do mundo inteiro. [SCREENSHOT: 00:01:30]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Passo 2: Planeje suas batalhas",
|
"title": "A Descoberta da Batata pelos Europeus",
|
||||||
"content": "Não é sempre a melhor ideia correr direto para a batalha. Em vez disso, tire um momento para formular um plano de ação antes de se aproximar do inimigo. Por exemplo, se você encontrar um acampamento de inimigos, pense sobre como você pode eliminá-los com o mínimo de problemas. [SCREENSHOT: 00:01:20]"
|
"content": "Os europeus descobriram a batata quando chegaram à América, mais especificamente nos Andes. A batata é originária da América, e foi a população local que ensinou os europeus a comê-la. Além da batata, outros alimentos importantes, como o milho e o cacau, também são originários da América. [SCREENSHOT: 00:03:45]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Passo 3: Gerencie seus equipamentos",
|
"title": "A Importância da Química na Vida Cotidiana",
|
||||||
"content": "Os equipamentos e armas do jogo têm uma mecânica de durabilidade, o que significa que eles se desgastam com o tempo e eventualmente quebram. Certifique-se de usar seus equipamentos mais poderosos apenas contra inimigos mais difíceis e chefs. [SCREENSHOT: 00:02:30]"
|
"content": "A química está presente em todos os aspectos da vida, desde a culinária até a indústria. Um exemplo interessante é o funcionamento de um spray de tinta. A bolinha dentro do spray serve para misturar a tinta com o propelente, um gás inflamável, garantindo que a tinta saia uniformemente. [SCREENSHOT: 00:06:15]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Passo 4: Escolha suas batalhas",
|
"title": "A Diferença entre Água com Gás e Água Oxigenada",
|
||||||
"content": "Não há vergonha em fugir de uma batalha se não há um objetivo em mente. Seus recursos de armas e equipamentos são limitados, então é sempre melhor evitar encontros desnecessários. [SCREENSHOT: 00:03:20]"
|
"content": "A água com gás contém bolhinhas de gás carbônico, enquanto a água oxigenada é uma substância química diferente, com uma molécula de H2O2. A água oxigenada não é simplesmente água com oxigênio dissolvido, mas sim uma substância com propriedades únicas. [SCREENSHOT: 00:09:30]"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Passo 5: Complete os santuários",
|
"title": "Conclusão e Novos Projetos",
|
||||||
"content": "Os santuários oferecem uma ótima distração dos períodos prolongados de exploração e completá-los pode recompensar você com espíritos orbitais, que podem ser usados para comprar upgrades úteis. Além disso, completar um santuário permite que você desbloqueie sua localização como um ponto de viagem rápida. [SCREENSHOT: 00:04:30]"
|
"content": "A química é uma ciência fascinante que pode ser aprendida de maneira divertida. A equipe do Manual do Mundo está trabalhando em novos projetos, incluindo a revisão do Grande Livro de Biologia. Esses livros são uma ótima forma de aprender sobre ciência de maneira interativa e divertida. [SCREENSHOT: 00:12:00]"
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Passo 6: Evite os guardiões",
|
|
||||||
"title": "Os guardiões são inimigos mortais que você deve evitar a todo custo, pelo menos no início do jogo. Se você precisar lutar contra um guardião, certifique-se de estar montado a cavalo e equipado com uma arsenal de bombas e flechas elementais. [SCREENSHOT: 00:05:40]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Passo 7: Cozinhe para sobreviver",
|
|
||||||
"content": "A culinária é uma parte essencial da sua sobrevivência e pode ajudá-lo de muitas maneiras. Para cozinhar, basta escolher alguns ingredientes do seu inventário e jogá-los em uma fogueira com uma panela. Dependendo dos ingredientes que você escolher, você pode criar um prato ou elixir que oferte um efeito de status diferente. [SCREENSHOT: 00:06:50]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Passo 8: Use armas pesadas contra inimigos com escudos",
|
|
||||||
"content": "Se um inimigo estiver usando um escudo, você pode usar uma arma pesada como um machado ou um martelo para derrubar o escudo e torná-lo vulnerável ao ataque. [SCREENSHOT: 00:07:40]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Passo 9: Ataque inimigos desprevenidos",
|
|
||||||
"content": "Se você se aproximar de um inimigo por trás, um prompt aparecerá, permitindo que você dê um golpe devastador enquanto o inimigo está desprevenido. Essa manobra é muito útil e pode matar inimigos em um único golpe, independentemente do seu nível de saúde e defesa. [SCREENSHOT: 00:08:30]"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Passo 10: Evite riscos desnecessários",
|
|
||||||
"content": "Não use nada de metal durante uma tempestade, pois isso pode ser perigoso. Além disso, certifique-se de seguir as dicas anteriores para minimizar os riscos e maximizar as recompensas. [SCREENSHOT: 00:09:20]"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Sections
|
|
||||||
### Passo 1: Introdução
|
|
||||||
Bem-vindo ao tutorial de The Legend of Zelda: Breath of the Wild. Neste vídeo, vamos explorar 9 dicas para iniciantes para ajudá-los a superar as ameaças iniciais do jogo.
|
|
||||||
**Timestamp:** 00:00:20
|
|
||||||
|
|
||||||
### Passo 2: Planeje suas batalhas
|
|
||||||
Não é sempre a melhor ideia correr direto para a batalha. Em vez disso, tire um momento para formular um plano de ação antes de se aproximar do inimigo. Por exemplo, se você encontrar um acampamento de inimigos, pense sobre como você pode eliminá-los com o mínimo de problemas.
|
|
||||||
**Timestamp:** 00:01:20
|
|
||||||
|
|
||||||
### Passo 3: Gerencie seus equipamentos
|
|
||||||
Os equipamentos e armas do jogo têm uma mecânica de durabilidade, o que significa que eles se desgastam com o tempo e eventualmente quebram. Certifique-se de usar seus equipamentos mais poderosos apenas contra inimigos mais difíceis e chefs.
|
|
||||||
**Timestamp:** 00:02:30
|
|
||||||
|
|
||||||
### Passo 4: Escolha suas batalhas
|
|
||||||
Não há vergonha em fugir de uma batalha se não há um objetivo em mente. Seus recursos de armas e equipamentos são limitados, então é sempre melhor evitar encontros desnecessários.
|
|
||||||
**Timestamp:** 00:03:20
|
|
||||||
|
|
||||||
### Passo 5: Complete os santuários
|
|
||||||
Os santuários oferecem uma ótima distração dos períodos prolongados de exploração e completá-los pode recompensar você com espíritos orbitais, que podem ser usados para comprar upgrades úteis. Além disso, completar um santuário permite que você desbloqueie sua localização como um ponto de viagem rápida.
|
|
||||||
**Timestamp:** 00:04:30
|
|
||||||
@ -92,6 +92,21 @@ app.UseCors("AllowAll");
|
|||||||
|
|
||||||
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));
|
app.MapGet("/health", () => Results.Ok(new { status = "healthy", timestamp = DateTime.UtcNow }));
|
||||||
|
|
||||||
|
// Video info endpoint (lightweight preview)
|
||||||
|
app.MapGet("/api/video-info", async (string url, AnalysisService service) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var info = await service.GetVideoInfoAsync(url);
|
||||||
|
return Results.Ok(info);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(new { error = ex.Message });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.WithName("GetVideoInfo");
|
||||||
|
|
||||||
// Main analysis endpoint
|
// Main analysis endpoint
|
||||||
app.MapPost("/api/analyze", async (AnalysisRequest request, AnalysisService service) =>
|
app.MapPost("/api/analyze", async (AnalysisRequest request, AnalysisService service) =>
|
||||||
{
|
{
|
||||||
|
|||||||
@ -70,6 +70,46 @@ public class AnalysisService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<VideoInfo> GetVideoInfoAsync(string url)
|
||||||
|
{
|
||||||
|
var ytDlpPath = GetYtDlpPath();
|
||||||
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = ytDlpPath,
|
||||||
|
Arguments = $"--print title --print channel --print duration --print thumbnail \"{url}\"",
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using var proc = Process.Start(startInfo) ?? throw new Exception("Failed to start yt-dlp");
|
||||||
|
var output = await proc.StandardOutput.ReadToEndAsync();
|
||||||
|
var error = await proc.StandardError.ReadToEndAsync();
|
||||||
|
await proc.WaitForExitAsync();
|
||||||
|
|
||||||
|
if (proc.ExitCode != 0)
|
||||||
|
throw new Exception($"yt-dlp error: {error}");
|
||||||
|
|
||||||
|
var lines = output.Trim().Split('\n', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
var title = lines.Length > 0 ? lines[0].Trim() : "Unknown";
|
||||||
|
var channel = lines.Length > 1 ? lines[1].Trim() : "Unknown";
|
||||||
|
var durationStr = lines.Length > 2 ? lines[2].Trim() : "0";
|
||||||
|
var thumbnail = lines.Length > 3 ? lines[3].Trim() : "";
|
||||||
|
|
||||||
|
double.TryParse(durationStr, System.Globalization.NumberStyles.Any,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture, out var durationSeconds);
|
||||||
|
|
||||||
|
return new VideoInfo
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Author = channel,
|
||||||
|
Duration = TimeSpan.FromSeconds(durationSeconds),
|
||||||
|
Url = url,
|
||||||
|
ThumbnailUrl = thumbnail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<AnalysisResponse> AnalyzeVideoAsync(AnalysisRequest request)
|
public async Task<AnalysisResponse> AnalyzeVideoAsync(AnalysisRequest request)
|
||||||
{
|
{
|
||||||
_debugSteps.Clear();
|
_debugSteps.Clear();
|
||||||
@ -83,22 +123,21 @@ public class AnalysisService
|
|||||||
{
|
{
|
||||||
// --- Step 1: Transcription ---
|
// --- Step 1: Transcription ---
|
||||||
AddLog("🌐 Obtendo transcrição via yt-dlp...");
|
AddLog("🌐 Obtendo transcrição via yt-dlp...");
|
||||||
var transcript = await GetTranscriptViaYtDlpAsync(request.VideoUrl, request.Language, tempDir);
|
var (transcript, originalTitle) = await GetTranscriptViaYtDlpAsync(request.VideoUrl, request.Language, tempDir);
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(transcript))
|
if (string.IsNullOrWhiteSpace(transcript))
|
||||||
throw new Exception("Não foi possível obter a transcrição do vídeo.");
|
throw new Exception("Não foi possível obter a transcrição do vídeo.");
|
||||||
|
|
||||||
AddLog($"✅ Transcrição obtida ({transcript.Length} caracteres).");
|
AddLog($"✅ Transcrição obtida: '{originalTitle}' ({transcript.Length} chars).");
|
||||||
|
|
||||||
// --- Step 2: Intelligence ---
|
// --- Step 2: Intelligence ---
|
||||||
AddLog("🧠 Enviando transcrição para o Groq (LLM)...");
|
AddLog("🧠 Enviando transcrição para o Groq (LLM)...");
|
||||||
var (tutorialSections, rawJson) = await GenerateTutorialContentAsync(transcript, request.Language);
|
var (tutorialSections, rawJson, category, docTitle) = await GenerateTutorialContentAsync(transcript, originalTitle, request.Language, request.OutputLanguage);
|
||||||
rawLlmResponse = rawJson;
|
rawLlmResponse = rawJson;
|
||||||
|
|
||||||
// Save debug MD in project root
|
// Save debug MD
|
||||||
var debugFile = Path.Combine(Directory.GetCurrentDirectory(), "DEBUG_LAST_RESPONSE.md");
|
var debugFile = Path.Combine(Directory.GetCurrentDirectory(), "DEBUG_LAST_RESPONSE.md");
|
||||||
var debugContent = $"# Debug Groq Response\n\nURL: {request.VideoUrl}\n\n## Raw JSON\n```json\n{rawJson}\n```\n\n## Sections\n" +
|
var debugContent = $"# {docTitle} ({category})\n\nSource: {originalTitle}\n\n## Raw JSON\n```json\n{rawJson}\n```\n";
|
||||||
string.Join("\n\n", tutorialSections.Select(s => $"### {s.Title}\n{s.Content}\n**Timestamp:** {s.ImageTimestamp}"));
|
|
||||||
await File.WriteAllTextAsync(debugFile, debugContent);
|
await File.WriteAllTextAsync(debugFile, debugContent);
|
||||||
AddLog($"📝 Arquivo de debug gerado: {debugFile}");
|
AddLog($"📝 Arquivo de debug gerado: {debugFile}");
|
||||||
|
|
||||||
@ -116,14 +155,16 @@ public class AnalysisService
|
|||||||
|
|
||||||
// --- Step 4: PDF Generation ---
|
// --- Step 4: PDF Generation ---
|
||||||
AddLog("📄 Gerando PDF final com QuestPDF...");
|
AddLog("📄 Gerando PDF final com QuestPDF...");
|
||||||
var pdfBytes = GeneratePdf(request.VideoUrl, tutorialSections);
|
var pdfBytes = GeneratePdf(docTitle, request.VideoUrl, tutorialSections, category);
|
||||||
|
|
||||||
AddLog("🎉 Processamento concluído com sucesso!");
|
AddLog("🎉 Processamento concluído com sucesso!");
|
||||||
|
|
||||||
return new AnalysisResponse
|
return new AnalysisResponse
|
||||||
{
|
{
|
||||||
Status = "success",
|
Status = "success",
|
||||||
VideoTitle = request.VideoUrl,
|
VideoTitle = originalTitle,
|
||||||
|
DocumentTitle = docTitle,
|
||||||
|
Category = category,
|
||||||
Transcript = transcript,
|
Transcript = transcript,
|
||||||
TutorialSections = tutorialSections,
|
TutorialSections = tutorialSections,
|
||||||
PdfData = pdfBytes,
|
PdfData = pdfBytes,
|
||||||
@ -152,10 +193,30 @@ public class AnalysisService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> GetTranscriptViaYtDlpAsync(string url, string language, string outputDir)
|
private async Task<(string transcript, string title)> GetTranscriptViaYtDlpAsync(string url, string language, string outputDir)
|
||||||
{
|
{
|
||||||
var ytDlpPath = GetYtDlpPath();
|
var ytDlpPath = GetYtDlpPath();
|
||||||
var arguments = $"--skip-download --write-sub --write-auto-sub --sub-lang {language},en --sub-format vtt --output \"%(title)s\" \"{url}\"";
|
// Use a safe output template to avoid filesystem issues, but we want the title.
|
||||||
|
// Better: Fetch title separately or read metadata json.
|
||||||
|
// Let's stick to filename trick but ensure safe chars.
|
||||||
|
// Actually, just fetch title with --print title
|
||||||
|
|
||||||
|
// 1. Fetch Title
|
||||||
|
var titleStartInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = ytDlpPath,
|
||||||
|
Arguments = $"--print title \"{url}\"",
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
var pTitle = Process.Start(titleStartInfo);
|
||||||
|
var title = (await pTitle!.StandardOutput.ReadToEndAsync()).Trim();
|
||||||
|
await pTitle.WaitForExitAsync();
|
||||||
|
if (string.IsNullOrEmpty(title)) title = "Video Analysis";
|
||||||
|
|
||||||
|
// 2. Fetch Subs
|
||||||
|
var arguments = $"--skip-download --write-sub --write-auto-sub --sub-lang {language},en --sub-format vtt --output \"%(id)s\" \"{url}\"";
|
||||||
|
|
||||||
var startInfo = new ProcessStartInfo
|
var startInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
@ -172,9 +233,9 @@ public class AnalysisService
|
|||||||
await proc.WaitForExitAsync();
|
await proc.WaitForExitAsync();
|
||||||
|
|
||||||
var vttFile = Directory.GetFiles(outputDir, "*.vtt").FirstOrDefault();
|
var vttFile = Directory.GetFiles(outputDir, "*.vtt").FirstOrDefault();
|
||||||
if (vttFile == null) return string.Empty;
|
if (vttFile == null) return (string.Empty, title);
|
||||||
|
|
||||||
return ParseVttToText(await File.ReadAllTextAsync(vttFile));
|
return (ParseVttToText(await File.ReadAllTextAsync(vttFile)), title);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ParseVttToText(string vttContent)
|
private string ParseVttToText(string vttContent)
|
||||||
@ -193,35 +254,67 @@ public class AnalysisService
|
|||||||
return string.Join(" ", textLines);
|
return string.Join(" ", textLines);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(List<TutorialSection> sections, string rawJson)> GenerateTutorialContentAsync(string transcript, string language)
|
private async Task<(List<TutorialSection> sections, string rawJson, string category, string docTitle)> GenerateTutorialContentAsync(string transcript, string originalTitle, string inputLanguage, string? outputLanguage)
|
||||||
{
|
{
|
||||||
|
var langMap = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{"en", "English"}, {"pt", "Portuguese (Brazilian)"}, {"es", "Spanish"},
|
||||||
|
{"fr", "French"}, {"de", "German"}, {"it", "Italian"},
|
||||||
|
{"ja", "Japanese"}, {"ko", "Korean"}, {"zh", "Chinese"}
|
||||||
|
};
|
||||||
|
var outputLang = string.IsNullOrWhiteSpace(outputLanguage) ? inputLanguage : outputLanguage;
|
||||||
|
var outputLangName = langMap.GetValueOrDefault(outputLang, outputLang);
|
||||||
|
|
||||||
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
|
var chatService = _kernel.GetRequiredService<IChatCompletionService>();
|
||||||
var prompt = $@"
|
var prompt = $@"
|
||||||
Converta a transcrição abaixo em um Tutorial Passo a Passo em {language}.
|
Você é um Editor Chefe e Analista de Conteúdo Sênior.
|
||||||
REGRAS:
|
Receberá:
|
||||||
1. Divida em passos lógicos.
|
A) TÍTULO ORIGINAL: {originalTitle}
|
||||||
2. Identifique onde um print da tela é necessário e insira [SCREENSHOT: HH:MM:SS].
|
B) TRANSCRIÇÃO: {transcript[..Math.Min(transcript.Length, 20000)]}
|
||||||
3. Retorne APENAS JSON.
|
|
||||||
JSON:
|
SUA MISSÃO:
|
||||||
|
1. **Classificar** o vídeo em: 'TUTORIAL', 'MEETING', 'LECTURE' ou 'OTHER'.
|
||||||
|
2. **Criar um Título Profissional**:
|
||||||
|
- Use o TÍTULO ORIGINAL como base.
|
||||||
|
- Remova clickbaits, emojis e CAPS LOCK excessivo.
|
||||||
|
- O título deve parecer o de um documento técnico ou ata oficial.
|
||||||
|
3. **Estruturar o Conteúdo**:
|
||||||
|
- Converta o conteúdo em um texto educativo e denso.
|
||||||
|
- Identifique momentos visuais críticos e insira `[SCREENSHOT: HH:MM:SS]` no final do parágrafo correspondente.
|
||||||
|
|
||||||
|
**IMPORTANTE: Todo o texto de saída (documentTitle, títulos das seções e conteúdo) DEVE ser escrito em {outputLangName}.**
|
||||||
|
|
||||||
|
SAÍDA JSON OBRIGATÓRIA:
|
||||||
{{
|
{{
|
||||||
|
""category"": ""TUTORIAL | MEETING | LECTURE | OTHER"",
|
||||||
|
""documentTitle"": ""Título Profissional Gerado"",
|
||||||
""sections"": [
|
""sections"": [
|
||||||
{{ ""title"": ""Passo 1"", ""content"": ""Texto... [SCREENSHOT: 00:01:20]"" }}
|
{{
|
||||||
]
|
""title"": ""Título da Seção"",
|
||||||
|
""content"": ""Texto explicativo detalhado... [SCREENSHOT: 00:05:30]""
|
||||||
}}
|
}}
|
||||||
Transcrição: {transcript[..Math.Min(transcript.Length, 15000)]}";
|
]
|
||||||
|
}}";
|
||||||
|
|
||||||
var result = await chatService.GetChatMessageContentAsync(prompt);
|
var result = await chatService.GetChatMessageContentAsync(prompt);
|
||||||
var json = result.Content?.Trim() ?? "{}";
|
var json = result.Content?.Trim() ?? "{}";
|
||||||
if (json.StartsWith("```")) {
|
// Extract JSON from LLM response — handles text before/after the JSON block
|
||||||
var idx = json.IndexOf('\n');
|
var jsonMatch = Regex.Match(json, @"\{[\s\S]*\}", RegexOptions.Singleline);
|
||||||
if (idx > 0) json = json[(idx+1)..];
|
if (jsonMatch.Success)
|
||||||
if (json.EndsWith("```")) json = json[..^3];
|
json = jsonMatch.Value;
|
||||||
}
|
|
||||||
|
|
||||||
var sections = new List<TutorialSection>();
|
var sections = new List<TutorialSection>();
|
||||||
|
string category = "OTHER";
|
||||||
|
string docTitle = originalTitle;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
using var doc = JsonDocument.Parse(json);
|
using var doc = JsonDocument.Parse(json);
|
||||||
foreach (var el in doc.RootElement.GetProperty("sections").EnumerateArray()) {
|
var root = doc.RootElement;
|
||||||
|
|
||||||
|
if (root.TryGetProperty("category", out var catEl)) category = catEl.GetString() ?? "OTHER";
|
||||||
|
if (root.TryGetProperty("documentTitle", out var titleEl)) docTitle = titleEl.GetString() ?? originalTitle;
|
||||||
|
|
||||||
|
foreach (var el in root.GetProperty("sections").EnumerateArray()) {
|
||||||
var content = el.GetProperty("content").GetString() ?? "";
|
var content = el.GetProperty("content").GetString() ?? "";
|
||||||
var ts = ExtractTimestamp(content);
|
var ts = ExtractTimestamp(content);
|
||||||
sections.Add(new TutorialSection {
|
sections.Add(new TutorialSection {
|
||||||
@ -231,7 +324,7 @@ Transcrição: {transcript[..Math.Min(transcript.Length, 15000)]}";
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch { }
|
} catch { }
|
||||||
return (sections, json);
|
return (sections, json, category, docTitle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string? ExtractTimestamp(string text)
|
private string? ExtractTimestamp(string text)
|
||||||
@ -304,28 +397,64 @@ Transcrição: {transcript[..Math.Min(transcript.Length, 15000)]}";
|
|||||||
catch (Exception ex) { AddLog($"❌ Erro Puppeteer: {ex.Message}"); }
|
catch (Exception ex) { AddLog($"❌ Erro Puppeteer: {ex.Message}"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] GeneratePdf(string videoUrl, List<TutorialSection> sections)
|
private byte[] GeneratePdf(string docTitle, string videoUrl, List<TutorialSection> sections, string category)
|
||||||
{
|
{
|
||||||
|
var categoryColor = category switch
|
||||||
|
{
|
||||||
|
"TUTORIAL" => Colors.Green.Medium,
|
||||||
|
"MEETING" => Colors.Orange.Medium,
|
||||||
|
"LECTURE" => Colors.Purple.Medium,
|
||||||
|
_ => Colors.Blue.Medium
|
||||||
|
};
|
||||||
|
|
||||||
var document = Document.Create(container =>
|
var document = Document.Create(container =>
|
||||||
{
|
{
|
||||||
container.Page(page =>
|
container.Page(page =>
|
||||||
{
|
{
|
||||||
page.Margin(2, Unit.Centimetre);
|
page.Margin(2, Unit.Centimetre);
|
||||||
page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Arial"));
|
page.DefaultTextStyle(x => x.FontSize(11).FontFamily("Segoe UI").Fallback(f => f.FontFamily("Microsoft YaHei")));
|
||||||
page.Header().Text("Video Tutorial").SemiBold().FontSize(24).FontColor(Colors.Blue.Medium);
|
|
||||||
|
page.Header().Column(c =>
|
||||||
|
{
|
||||||
|
c.Item().Row(row =>
|
||||||
|
{
|
||||||
|
row.RelativeItem().Text(docTitle).SemiBold().FontSize(20).FontColor(Colors.Black);
|
||||||
|
row.ConstantItem(100).AlignRight().Text(category).Bold().FontSize(10).FontColor(categoryColor);
|
||||||
|
});
|
||||||
|
c.Item().PaddingTop(5).LineHorizontal(1).LineColor(Colors.Grey.Lighten2);
|
||||||
|
});
|
||||||
|
|
||||||
page.Content().PaddingVertical(1, Unit.Centimetre).Column(column =>
|
page.Content().PaddingVertical(1, Unit.Centimetre).Column(column =>
|
||||||
{
|
{
|
||||||
column.Item().Text($"Fonte: {videoUrl}").Italic().FontSize(10).FontColor(Colors.Grey.Medium);
|
column.Item().Text($"Fonte: {videoUrl}").Italic().FontSize(9).FontColor(Colors.Grey.Medium);
|
||||||
column.Item().PaddingBottom(20);
|
column.Item().PaddingBottom(20);
|
||||||
|
|
||||||
foreach (var section in sections)
|
foreach (var section in sections)
|
||||||
{
|
{
|
||||||
column.Item().Text(section.Title).Bold().FontSize(16);
|
column.Item().Text(section.Title).Bold().FontSize(14).FontColor(categoryColor);
|
||||||
column.Item().Text(text => { text.Span(section.Content); });
|
column.Item().Text(text => { text.Span(section.Content); });
|
||||||
|
|
||||||
if (section.ImageData != null)
|
if (section.ImageData != null)
|
||||||
column.Item().PaddingVertical(15).Image(section.ImageData).FitWidth();
|
{
|
||||||
column.Item().PaddingBottom(20);
|
column.Item().PaddingVertical(10).Image(section.ImageData).FitWidth();
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrEmpty(section.ImageTimestamp))
|
||||||
|
{
|
||||||
|
// Placeholder for missing image (Graceful Degradation)
|
||||||
|
column.Item().PaddingVertical(10)
|
||||||
|
.Background(Colors.Grey.Lighten3)
|
||||||
|
.Height(100)
|
||||||
|
.AlignCenter()
|
||||||
|
.AlignMiddle()
|
||||||
|
.Text($"[Imagem Indisponível: {section.ImageTimestamp}]")
|
||||||
|
.FontSize(10)
|
||||||
|
.FontColor(Colors.Grey.Darken2);
|
||||||
|
}
|
||||||
|
|
||||||
|
column.Item().PaddingBottom(15);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
page.Footer().AlignCenter().Text(x => { x.Span("Gerado por VideoStudy.app - "); x.CurrentPageNumber(); });
|
page.Footer().AlignCenter().Text(x => { x.Span("Gerado por VideoStudy.app - "); x.CurrentPageNumber(); });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
|
||||||
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
7
VideoStudy.Native/App.xaml
Normal file
7
VideoStudy.Native/App.xaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version = "1.0" encoding = "UTF-8" ?>
|
||||||
|
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="VideoStudy.Native.App">
|
||||||
|
<Application.Resources>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
14
VideoStudy.Native/App.xaml.cs
Normal file
14
VideoStudy.Native/App.xaml.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
namespace VideoStudy.Native;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Window CreateWindow(IActivationState? activationState)
|
||||||
|
{
|
||||||
|
return new Window(new MainPage());
|
||||||
|
}
|
||||||
|
}
|
||||||
15
VideoStudy.Native/MainPage.xaml
Normal file
15
VideoStudy.Native/MainPage.xaml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:blazor="clr-namespace:Microsoft.AspNetCore.Components.WebView.Maui;assembly=Microsoft.AspNetCore.Components.WebView.Maui"
|
||||||
|
xmlns:ui="clr-namespace:VideoStudy.UI;assembly=VideoStudy.UI"
|
||||||
|
x:Class="VideoStudy.Native.MainPage"
|
||||||
|
BackgroundColor="White">
|
||||||
|
|
||||||
|
<blazor:BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
|
||||||
|
<blazor:BlazorWebView.RootComponents>
|
||||||
|
<blazor:RootComponent Selector="#app" ComponentType="{x:Type ui:Routes}" />
|
||||||
|
</blazor:BlazorWebView.RootComponents>
|
||||||
|
</blazor:BlazorWebView>
|
||||||
|
|
||||||
|
</ContentPage>
|
||||||
9
VideoStudy.Native/MainPage.xaml.cs
Normal file
9
VideoStudy.Native/MainPage.xaml.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
namespace VideoStudy.Native;
|
||||||
|
|
||||||
|
public partial class MainPage : ContentPage
|
||||||
|
{
|
||||||
|
public MainPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
37
VideoStudy.Native/MauiProgram.cs
Normal file
37
VideoStudy.Native/MauiProgram.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.WebView.Maui;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using VideoStudy.Native.Services;
|
||||||
|
using VideoStudy.Shared;
|
||||||
|
using VideoStudy.UI;
|
||||||
|
|
||||||
|
namespace VideoStudy.Native;
|
||||||
|
|
||||||
|
public static class MauiProgram
|
||||||
|
{
|
||||||
|
public static MauiApp CreateMauiApp()
|
||||||
|
{
|
||||||
|
var builder = MauiApp.CreateBuilder();
|
||||||
|
builder
|
||||||
|
.UseMauiApp<App>();
|
||||||
|
|
||||||
|
builder.Services.AddMauiBlazorWebView();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
builder.Services.AddBlazorWebViewDeveloperTools();
|
||||||
|
builder.Logging.AddDebug();
|
||||||
|
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Configurar HttpClient para API Local (timeout longo para análise de vídeo)
|
||||||
|
builder.Services.AddScoped(sp => new HttpClient
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost:5000"),
|
||||||
|
Timeout = TimeSpan.FromMinutes(10)
|
||||||
|
});
|
||||||
|
|
||||||
|
// PDF Saver (FileSavePicker do Windows)
|
||||||
|
builder.Services.AddSingleton<IPdfSaver, WindowsPdfSaver>();
|
||||||
|
|
||||||
|
return builder.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
VideoStudy.Native/Platforms/Windows/App.xaml
Normal file
7
VideoStudy.Native/Platforms/Windows/App.xaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<maui:MauiWinUIApplication
|
||||||
|
x:Class="VideoStudy.Native.WinUI.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:maui="using:Microsoft.Maui"
|
||||||
|
xmlns:local="using:VideoStudy.Native.WinUI">
|
||||||
|
</maui:MauiWinUIApplication>
|
||||||
13
VideoStudy.Native/Platforms/Windows/App.xaml.cs
Normal file
13
VideoStudy.Native/Platforms/Windows/App.xaml.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.Maui;
|
||||||
|
using Microsoft.Maui.Hosting;
|
||||||
|
|
||||||
|
namespace VideoStudy.Native.WinUI;
|
||||||
|
|
||||||
|
public partial class App : MauiWinUIApplication
|
||||||
|
{
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||||
|
}
|
||||||
49
VideoStudy.Native/Platforms/Windows/Package.appxmanifest
Normal file
49
VideoStudy.Native/Platforms/Windows/Package.appxmanifest
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Package
|
||||||
|
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||||
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
|
IgnorableNamespaces="uap rescap">
|
||||||
|
|
||||||
|
<Identity
|
||||||
|
Name="com.companyname.videostudy"
|
||||||
|
Publisher="CN=Ricardo"
|
||||||
|
Version="1.0.0.0" />
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<DisplayName>VideoStudy</DisplayName>
|
||||||
|
<PublisherDisplayName>Ricardo</PublisherDisplayName>
|
||||||
|
<Logo>Resources\AppIcon\appicon.svg</Logo>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
</Dependencies>
|
||||||
|
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="x-generate"/>
|
||||||
|
</Resources>
|
||||||
|
|
||||||
|
<Applications>
|
||||||
|
<Application Id="App"
|
||||||
|
Executable="$targetnametoken$.exe"
|
||||||
|
EntryPoint="$targetentrypoint$">
|
||||||
|
<uap:VisualElements
|
||||||
|
DisplayName="VideoStudy"
|
||||||
|
Description="VideoStudy"
|
||||||
|
BackgroundColor="transparent"
|
||||||
|
Square150x150Logo="Resources\AppIcon\appicon.svg"
|
||||||
|
Square44x44Logo="Resources\AppIcon\appicon.svg">
|
||||||
|
<uap:DefaultTile Wide310x150Logo="Resources\AppIcon\appicon.svg" />
|
||||||
|
<uap:SplashScreen Image="Resources\Splash\splash.svg" />
|
||||||
|
</uap:VisualElements>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
|
||||||
|
<Capabilities>
|
||||||
|
<rescap:Capability Name="runFullTrust" />
|
||||||
|
<Capability Name="internetClient" />
|
||||||
|
</Capabilities>
|
||||||
|
|
||||||
|
</Package>
|
||||||
4
VideoStudy.Native/Resources/AppIcon/appicon.svg
Normal file
4
VideoStudy.Native/Resources/AppIcon/appicon.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="64" cy="64" r="60" fill="#512BD4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 200 B |
4
VideoStudy.Native/Resources/AppIcon/appiconfg.svg
Normal file
4
VideoStudy.Native/Resources/AppIcon/appiconfg.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="40" fill="white">VS</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 280 B |
4
VideoStudy.Native/Resources/Images/dotnet_bot.svg
Normal file
4
VideoStudy.Native/Resources/Images/dotnet_bot.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="168" height="208" viewBox="0 0 168 208" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="168" height="208" fill="#512BD4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 200 B |
5
VideoStudy.Native/Resources/Splash/splash.svg
Normal file
5
VideoStudy.Native/Resources/Splash/splash.svg
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="128" height="128" fill="#512BD4"/>
|
||||||
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="20" fill="white">VideoStudy</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
26
VideoStudy.Native/Resources/Styles/Colors.xaml
Normal file
26
VideoStudy.Native/Resources/Styles/Colors.xaml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<?xaml-comp compile="true" ?>
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||||
|
|
||||||
|
<Color x:Key="Primary">#512BD4</Color>
|
||||||
|
<Color x:Key="Secondary">#DFD8F7</Color>
|
||||||
|
<Color x:Key="Tertiary">#2B0B98</Color>
|
||||||
|
<Color x:Key="White">#FFFFFF</Color>
|
||||||
|
<Color x:Key="Black">#000000</Color>
|
||||||
|
<Color x:Key="Gray100">#E1E1E1</Color>
|
||||||
|
<Color x:Key="Gray200">#C8C8C8</Color>
|
||||||
|
<Color x:Key="Gray300">#ACACAC</Color>
|
||||||
|
<Color x:Key="Gray400">#919191</Color>
|
||||||
|
<Color x:Key="Gray500">#6E6E6E</Color>
|
||||||
|
<Color x:Key="Gray600">#404040</Color>
|
||||||
|
<Color x:Key="Gray900">#212121</Color>
|
||||||
|
<Color x:Key="Gray950">#141414</Color>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
|
||||||
|
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
|
||||||
|
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
|
||||||
|
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
|
||||||
|
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
|
||||||
|
</ResourceDictionary>
|
||||||
27
VideoStudy.Native/Resources/Styles/Styles.xaml
Normal file
27
VideoStudy.Native/Resources/Styles/Styles.xaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<?xaml-comp compile="true" ?>
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||||
|
|
||||||
|
<Style TargetType="ActivityIndicator">
|
||||||
|
<Setter Property="Color" Value="{StaticResource Primary}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="TextColor" Value="{StaticResource White}" />
|
||||||
|
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
|
||||||
|
<Setter Property="FontSize" Value="14" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
<Setter Property="Padding" Value="14,10" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Label">
|
||||||
|
<Setter Property="TextColor" Value="{StaticResource Black}" />
|
||||||
|
<Setter Property="FontSize" Value="14" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="Page">
|
||||||
|
<Setter Property="BackgroundColor" Value="{StaticResource White}" />
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
38
VideoStudy.Native/Services/WindowsPdfSaver.cs
Normal file
38
VideoStudy.Native/Services/WindowsPdfSaver.cs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
using VideoStudy.Shared;
|
||||||
|
using Windows.Storage;
|
||||||
|
using Windows.Storage.Pickers;
|
||||||
|
using WinRT.Interop;
|
||||||
|
|
||||||
|
namespace VideoStudy.Native.Services;
|
||||||
|
|
||||||
|
public class WindowsPdfSaver : IPdfSaver
|
||||||
|
{
|
||||||
|
public async Task<string?> SavePdfAsync(byte[] pdfData, string suggestedFileName)
|
||||||
|
{
|
||||||
|
var savePicker = new FileSavePicker
|
||||||
|
{
|
||||||
|
SuggestedStartLocation = PickerLocationId.Downloads,
|
||||||
|
SuggestedFileName = suggestedFileName
|
||||||
|
};
|
||||||
|
savePicker.FileTypeChoices.Add("PDF", [".pdf"]);
|
||||||
|
|
||||||
|
// Get the window handle for the picker
|
||||||
|
var window = Application.Current?.Windows.FirstOrDefault();
|
||||||
|
var mauiWindow = window?.Handler?.PlatformView;
|
||||||
|
if (mauiWindow is Microsoft.UI.Xaml.Window winuiWindow)
|
||||||
|
{
|
||||||
|
var hwnd = WindowNative.GetWindowHandle(winuiWindow);
|
||||||
|
InitializeWithWindow.Initialize(savePicker, hwnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
var file = await savePicker.PickSaveFileAsync();
|
||||||
|
if (file == null) return null;
|
||||||
|
|
||||||
|
await FileIO.WriteBytesAsync(file, pdfData);
|
||||||
|
|
||||||
|
// Open the PDF after saving
|
||||||
|
await Windows.System.Launcher.LaunchFileAsync(file);
|
||||||
|
|
||||||
|
return file.Path;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
VideoStudy.Native/VideoStudy.Native.csproj
Normal file
39
VideoStudy.Native/VideoStudy.Native.csproj
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
|
||||||
|
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<UseMaui>true</UseMaui>
|
||||||
|
<SingleProject>true</SingleProject>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<ApplicationTitle>VideoStudy</ApplicationTitle>
|
||||||
|
<ApplicationId>com.companyname.videostudy</ApplicationId>
|
||||||
|
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||||
|
<ApplicationVersion>1</ApplicationVersion>
|
||||||
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
||||||
|
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
|
||||||
|
<MauiImage Include="Resources\Images\*" />
|
||||||
|
<MauiFont Include="Resources\Fonts\*" />
|
||||||
|
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.31" />
|
||||||
|
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="10.0.31" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.31" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\VideoStudy.UI\VideoStudy.UI.csproj" />
|
||||||
|
<ProjectReference Include="..\VideoStudy.Shared\VideoStudy.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
31
VideoStudy.Native/wwwroot/index.html
Normal file
31
VideoStudy.Native/wwwroot/index.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
|
<title>VideoStudy.Native</title>
|
||||||
|
<base href="/" />
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" />
|
||||||
|
<link rel="stylesheet" href="_content/VideoStudy.UI/app.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
<div style="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);">
|
||||||
|
<div class="spinner-border text-primary" role="status"></div>
|
||||||
|
<div style="margin-top:10px;">Carregando VideoStudy...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
An unhandled error has occurred.
|
||||||
|
<a href="" class="reload">Reload</a>
|
||||||
|
<a class="dismiss">🗙</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="_framework/blazor.webview.js"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -6,7 +6,8 @@ namespace VideoStudy.Shared;
|
|||||||
public class AnalysisRequest
|
public class AnalysisRequest
|
||||||
{
|
{
|
||||||
public string VideoUrl { get; set; } = string.Empty;
|
public string VideoUrl { get; set; } = string.Empty;
|
||||||
public string Language { get; set; } = "en";
|
public string Language { get; set; } = "en"; // Input language (subtitles)
|
||||||
|
public string? OutputLanguage { get; set; } // Output language (tutorial). null = same as Language
|
||||||
public string Mode { get; set; } = "fast"; // fast or advanced
|
public string Mode { get; set; } = "fast"; // fast or advanced
|
||||||
public string? TranscriptText { get; set; } // Optional: if client already transcribed it
|
public string? TranscriptText { get; set; } // Optional: if client already transcribed it
|
||||||
public double DurationSeconds { get; set; }
|
public double DurationSeconds { get; set; }
|
||||||
@ -18,6 +19,8 @@ public class AnalysisRequest
|
|||||||
public class AnalysisResponse
|
public class AnalysisResponse
|
||||||
{
|
{
|
||||||
public string VideoTitle { get; set; } = string.Empty;
|
public string VideoTitle { get; set; } = string.Empty;
|
||||||
|
public string DocumentTitle { get; set; } = string.Empty; // New
|
||||||
|
public string Category { get; set; } = "OTHER"; // New
|
||||||
public string Transcript { get; set; } = string.Empty;
|
public string Transcript { get; set; } = string.Empty;
|
||||||
public List<KeyMoment> KeyMoments { get; set; } = [];
|
public List<KeyMoment> KeyMoments { get; set; } = [];
|
||||||
public string Analysis { get; set; } = string.Empty;
|
public string Analysis { get; set; } = string.Empty;
|
||||||
@ -95,3 +98,8 @@ public class DownloadProgress
|
|||||||
public double PercentComplete { get; set; }
|
public double PercentComplete { get; set; }
|
||||||
public string Status { get; set; } = string.Empty;
|
public string Status { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface IPdfSaver
|
||||||
|
{
|
||||||
|
Task<string?> SavePdfAsync(byte[] pdfData, string suggestedFileName);
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
26
VideoStudy.UI/App.razor
Normal file
26
VideoStudy.UI/App.razor
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<base href="/" />
|
||||||
|
|
||||||
|
<!-- Bootstrap 5.3.2 CSS -->
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="app.css" />
|
||||||
|
<link rel="stylesheet" href="VideoStudy.Desktop.styles.css" />
|
||||||
|
<HeadOutlet />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<Routes />
|
||||||
|
|
||||||
|
<!-- Bootstrap 5.3.2 JS -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|
||||||
|
<script src="_framework/blazor.web.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
36
VideoStudy.UI/ExampleJsInterop.cs
Normal file
36
VideoStudy.UI/ExampleJsInterop.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
|
namespace VideoStudy.UI;
|
||||||
|
|
||||||
|
// This class provides an example of how JavaScript functionality can be wrapped
|
||||||
|
// in a .NET class for easy consumption. The associated JavaScript module is
|
||||||
|
// loaded on demand when first needed.
|
||||||
|
//
|
||||||
|
// This class can be registered as scoped DI service and then injected into Blazor
|
||||||
|
// components for use.
|
||||||
|
|
||||||
|
public class ExampleJsInterop : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly Lazy<Task<IJSObjectReference>> moduleTask;
|
||||||
|
|
||||||
|
public ExampleJsInterop(IJSRuntime jsRuntime)
|
||||||
|
{
|
||||||
|
moduleTask = new (() => jsRuntime.InvokeAsync<IJSObjectReference>(
|
||||||
|
"import", "./_content/VideoStudy.UI/exampleJsInterop.js").AsTask());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<string> Prompt(string message)
|
||||||
|
{
|
||||||
|
var module = await moduleTask.Value;
|
||||||
|
return await module.InvokeAsync<string>("showPrompt", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (moduleTask.IsValueCreated)
|
||||||
|
{
|
||||||
|
var module = await moduleTask.Value;
|
||||||
|
await module.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
VideoStudy.UI/Layout/MainLayout.razor
Normal file
58
VideoStudy.UI/Layout/MainLayout.razor
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
@inherits LayoutComponentBase
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
<nav class="navbar navbar-dark bg-dark sticky-top mb-4">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<span class="navbar-brand mb-0 h1">📺 VideoStudy</span>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main role="main" class="px-4">
|
||||||
|
@Body
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="blazor-error-ui">
|
||||||
|
<div class="alert alert-danger m-3" role="alert">
|
||||||
|
<h4 class="alert-heading">⚠️ Unhandled error</h4>
|
||||||
|
<p>An unhandled error has occurred. Please reload the page.</p>
|
||||||
|
<hr>
|
||||||
|
<button class="btn btn-primary reload">Reload</button>
|
||||||
|
<button class="btn btn-secondary dismiss">Dismiss</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 9999;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui[style*="display"] {
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reload, .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
18
VideoStudy.UI/Layout/MainLayout.razor.css
Normal file
18
VideoStudy.UI/Layout/MainLayout.razor.css
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#blazor-error-ui {
|
||||||
|
background: lightyellow;
|
||||||
|
bottom: 0;
|
||||||
|
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
display: none;
|
||||||
|
left: 0;
|
||||||
|
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#blazor-error-ui .dismiss {
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
33
VideoStudy.UI/Pages/Error.razor
Normal file
33
VideoStudy.UI/Pages/Error.razor
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@page "/Error"
|
||||||
|
@using System.Diagnostics
|
||||||
|
|
||||||
|
<PageTitle>Error</PageTitle>
|
||||||
|
|
||||||
|
<h1 class="text-danger">Error.</h1>
|
||||||
|
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||||
|
|
||||||
|
@if (ShowRequestId)
|
||||||
|
{
|
||||||
|
<p>
|
||||||
|
<strong>Request ID:</strong> <code>@RequestId</code>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Development Mode</h3>
|
||||||
|
<p>
|
||||||
|
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||||
|
It can result in displaying sensitive information from exceptions to end users.
|
||||||
|
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||||
|
and restarting the app.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
private string? RequestId { get; set; }
|
||||||
|
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
|
||||||
|
|
||||||
|
protected override void OnInitialized() =>
|
||||||
|
RequestId = Activity.Current?.Id;
|
||||||
|
}
|
||||||
327
VideoStudy.UI/Pages/Home.razor
Normal file
327
VideoStudy.UI/Pages/Home.razor
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
@page "/"
|
||||||
|
@inject HttpClient Http
|
||||||
|
@inject IPdfSaver PdfSaver
|
||||||
|
@using VideoStudy.Shared
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using System.Text.Json
|
||||||
|
|
||||||
|
<PageTitle>VideoStudy - Video Analysis</PageTitle>
|
||||||
|
|
||||||
|
<div class="container-fluid py-4">
|
||||||
|
<!-- Header -->
|
||||||
|
<header class="text-center mb-5">
|
||||||
|
<h1 class="display-4 fw-bold" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">
|
||||||
|
VideoStudy (Native)
|
||||||
|
</h1>
|
||||||
|
<p class="lead text-muted">Transforme videos em apostilas inteligentes com IA</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-8 mx-auto">
|
||||||
|
|
||||||
|
<!-- Tabs Navigation -->
|
||||||
|
<ul class="nav nav-pills nav-fill mb-4 shadow-sm p-1 bg-light rounded-pill">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link rounded-pill py-3 fw-bold @(activeTab == "youtube" ? "active bg-primary text-white" : "text-muted")"
|
||||||
|
@onclick="@(() => activeTab = "youtube")" disabled="@(isProcessing || isFetchingInfo)">
|
||||||
|
<span style="font-size:1.1rem;">YouTube Video</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="nav-link rounded-pill py-3 fw-bold text-muted" disabled>
|
||||||
|
Local Video (Coming Soon)
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- Tab Content -->
|
||||||
|
<div class="card shadow-soft border-0 rounded-4 overflow-hidden mb-4 p-4 bg-white">
|
||||||
|
@if (activeTab == "youtube")
|
||||||
|
{
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="form-label fw-bold text-muted small">YouTube URL</label>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input type="text" class="form-control form-control-lg" placeholder="https://www.youtube.com/watch?v=..."
|
||||||
|
@bind="videoUrl" disabled="@(isProcessing || isFetchingInfo)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Video Info Preview -->
|
||||||
|
@if (videoInfo != null)
|
||||||
|
{
|
||||||
|
<div class="border rounded-3 p-3 mb-4 d-flex align-items-start gap-3" style="background: #f0f4ff;">
|
||||||
|
@if (!string.IsNullOrEmpty(videoInfo.ThumbnailUrl))
|
||||||
|
{
|
||||||
|
<img src="@videoInfo.ThumbnailUrl" alt="Thumbnail"
|
||||||
|
style="width: 180px; border-radius: 8px; flex-shrink: 0;" />
|
||||||
|
}
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h6 class="fw-bold mb-1">@videoInfo.Title</h6>
|
||||||
|
<div class="text-muted small">
|
||||||
|
<span class="me-3">@videoInfo.Author</span>
|
||||||
|
<span>@videoInfo.Duration.ToString(@"hh\:mm\:ss")</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" @onclick="ClearVideoInfo"
|
||||||
|
disabled="@(isProcessing)" title="Limpar">X</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Language Selectors -->
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold text-muted small">Idioma do Video (Legendas)</label>
|
||||||
|
<select class="form-select" @bind="selectedLanguage" disabled="@(isProcessing || isFetchingInfo)">
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="pt">Portuguese (BR)</option>
|
||||||
|
<option value="es">Spanish</option>
|
||||||
|
<option value="fr">French</option>
|
||||||
|
<option value="de">German</option>
|
||||||
|
<option value="ja">Japanese</option>
|
||||||
|
<option value="ko">Korean</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label class="form-label fw-bold text-muted small">Idioma de Saida (Tutorial)</label>
|
||||||
|
<select class="form-select" @bind="selectedOutputLanguage" disabled="@(isProcessing || isFetchingInfo)">
|
||||||
|
<option value="pt">Portuguese (BR)</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="es">Spanish</option>
|
||||||
|
<option value="fr">French</option>
|
||||||
|
<option value="de">German</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="mt-4">
|
||||||
|
@if (videoInfo == null)
|
||||||
|
{
|
||||||
|
<!-- Step 1: Fetch video info -->
|
||||||
|
<button class="btn btn-lg btn-primary w-100 py-3 fw-bold shadow-sm"
|
||||||
|
@onclick="FetchVideoInfo"
|
||||||
|
disabled="@(isProcessing || isFetchingInfo || string.IsNullOrWhiteSpace(videoUrl))">
|
||||||
|
@if (isFetchingInfo)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
<span>Verificando video...</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Generate Tutorial PDF</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<!-- Step 2: Confirm and generate -->
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button class="btn btn-lg btn-success flex-grow-1 py-3 fw-bold shadow-sm"
|
||||||
|
@onclick="StartAnalysis"
|
||||||
|
disabled="@isProcessing">
|
||||||
|
@if (isProcessing)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
||||||
|
<span>Gerando PDF...</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Confirmar e Gerar PDF</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Indicator -->
|
||||||
|
@if (isProcessing || currentStep > 0)
|
||||||
|
{
|
||||||
|
<div class="progress mb-3" style="height: 25px;">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" style="width: @(progress)%">
|
||||||
|
@statusMessage
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Logs (Collapsible) -->
|
||||||
|
@if (logs.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="card shadow-sm mb-4 border-0 rounded-4 overflow-hidden">
|
||||||
|
<div class="card-header bg-dark text-white d-flex justify-content-between align-items-center" style="cursor: pointer;" @onclick="ToggleLogs">
|
||||||
|
<small class="fw-bold">Execution Logs</small>
|
||||||
|
<small>@logs.Count items @(showLogs ? "v" : ">")</small>
|
||||||
|
</div>
|
||||||
|
@if (showLogs)
|
||||||
|
{
|
||||||
|
<div class="card-body bg-light" style="max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 0.8rem;">
|
||||||
|
@foreach (var log in logs)
|
||||||
|
{
|
||||||
|
<div class="text-dark border-bottom py-1">
|
||||||
|
<span class="text-muted">[@log.Timestamp:HH:mm:ss]</span> @log.Message
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.nav-pills .nav-link.active {
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
.shadow-soft {
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string activeTab = "youtube";
|
||||||
|
private bool isProcessing = false;
|
||||||
|
private bool isFetchingInfo = false;
|
||||||
|
private int progress = 0;
|
||||||
|
private int currentStep = 0;
|
||||||
|
private string statusMessage = "Ready";
|
||||||
|
private bool showLogs = false;
|
||||||
|
private List<LogEntry> logs = new();
|
||||||
|
|
||||||
|
private string videoUrl = string.Empty;
|
||||||
|
private string selectedLanguage = "en";
|
||||||
|
private string selectedOutputLanguage = "pt";
|
||||||
|
private VideoInfo? videoInfo;
|
||||||
|
|
||||||
|
private class LogEntry
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; } = DateTime.Now;
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleLogs() => showLogs = !showLogs;
|
||||||
|
|
||||||
|
private void AddLog(string message)
|
||||||
|
{
|
||||||
|
logs.Insert(0, new LogEntry { Message = message });
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearVideoInfo()
|
||||||
|
{
|
||||||
|
videoInfo = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FetchVideoInfo()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(videoUrl)) return;
|
||||||
|
isFetchingInfo = true;
|
||||||
|
showLogs = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AddLog("Buscando informacoes do video...");
|
||||||
|
var response = await Http.GetAsync($"api/video-info?url={Uri.EscapeDataString(videoUrl)}");
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception($"API Error: {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
videoInfo = await response.Content.ReadFromJsonAsync<VideoInfo>(
|
||||||
|
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
|
|
||||||
|
if (videoInfo != null)
|
||||||
|
AddLog($"Video encontrado: {videoInfo.Title} ({videoInfo.Duration:hh\\:mm\\:ss})");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AddLog($"Erro: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isFetchingInfo = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartAnalysis()
|
||||||
|
{
|
||||||
|
isProcessing = true;
|
||||||
|
progress = 0;
|
||||||
|
currentStep = 0;
|
||||||
|
logs.Clear();
|
||||||
|
showLogs = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
currentStep = 1;
|
||||||
|
statusMessage = "Calling API...";
|
||||||
|
AddLog("Sending request to API...");
|
||||||
|
|
||||||
|
var request = new AnalysisRequest
|
||||||
|
{
|
||||||
|
VideoUrl = videoUrl,
|
||||||
|
Language = selectedLanguage,
|
||||||
|
OutputLanguage = selectedOutputLanguage,
|
||||||
|
Mode = "native"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Long timeout via CancellationToken (HttpClient.Timeout can't be changed after first request)
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(10));
|
||||||
|
|
||||||
|
var response = await Http.PostAsJsonAsync("api/analyze", request, cts.Token);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var error = await response.Content.ReadAsStringAsync();
|
||||||
|
throw new Exception($"API Error ({response.StatusCode}): {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonSerializer.Deserialize<AnalysisResponse>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||||
|
|
||||||
|
if (result == null) throw new Exception("Failed to deserialize response");
|
||||||
|
|
||||||
|
if (result.DebugSteps != null)
|
||||||
|
{
|
||||||
|
foreach (var step in result.DebugSteps) AddLog($"[API] {step}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.Status == "error") throw new Exception(result.ErrorMessage);
|
||||||
|
|
||||||
|
AddLog("Analysis Complete!");
|
||||||
|
progress = 100;
|
||||||
|
statusMessage = "Done!";
|
||||||
|
|
||||||
|
if (result.PdfData != null && result.PdfData.Length > 0)
|
||||||
|
{
|
||||||
|
AddLog("Saving PDF...");
|
||||||
|
var fileName = $"VideoStudy_{result.VideoTitle ?? "Tutorial"}_{DateTime.Now:yyyyMMdd_HHmmss}.pdf";
|
||||||
|
// Remove invalid filename chars
|
||||||
|
fileName = string.Join("_", fileName.Split(Path.GetInvalidFileNameChars()));
|
||||||
|
var savedPath = await PdfSaver.SavePdfAsync(result.PdfData, fileName);
|
||||||
|
if (savedPath != null)
|
||||||
|
AddLog($"PDF saved: {savedPath}");
|
||||||
|
else
|
||||||
|
AddLog("PDF save cancelled by user");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddLog("No PDF data returned from API");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
AddLog($"Error: {ex.Message}");
|
||||||
|
statusMessage = "Error";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isProcessing = false;
|
||||||
|
videoInfo = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
VideoStudy.UI/PdfPreview.razor
Normal file
43
VideoStudy.UI/PdfPreview.razor
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<div class="modal fade show d-block" tabindex="-1" style="background: rgba(0,0,0,0.5);">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content border-0 shadow-lg">
|
||||||
|
<div class="modal-header bg-gradient text-white">
|
||||||
|
<h5 class="modal-title">📄 PDF Preview</h5>
|
||||||
|
<button type="button" class="btn-close btn-close-white" @onclick="OnCancel"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body bg-light p-4 text-center">
|
||||||
|
<div class="pdf-icon mb-3">
|
||||||
|
<span style="font-size: 4rem;">📄</span>
|
||||||
|
</div>
|
||||||
|
<h5>Your Study Notes are ready!</h5>
|
||||||
|
<p class="text-muted">@System.IO.Path.GetFileName(PdfPath)</p>
|
||||||
|
|
||||||
|
<div class="alert alert-info text-start mt-3">
|
||||||
|
<strong>Saved to:</strong> <br/>
|
||||||
|
<small>@PdfPath</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" @onclick="OnCancel">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" @onclick="OpenPdf">📂 Open PDF</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string PdfPath { get; set; } = string.Empty;
|
||||||
|
[Parameter] public EventCallback OnCancel { get; set; }
|
||||||
|
|
||||||
|
private void OpenPdf()
|
||||||
|
{
|
||||||
|
// Platform specific open
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var p = new System.Diagnostics.Process();
|
||||||
|
p.StartInfo = new System.Diagnostics.ProcessStartInfo(PdfPath) { UseShellExecute = true };
|
||||||
|
p.Start();
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
VideoStudy.UI/ProgressIndicator.razor
Normal file
57
VideoStudy.UI/ProgressIndicator.razor
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<div class="progress-container mb-4">
|
||||||
|
<div class="d-flex justify-content-between mb-2">
|
||||||
|
<span class="fw-bold text-primary">@Status</span>
|
||||||
|
<span class="text-muted">@Percent%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 10px;">
|
||||||
|
<div class="progress-bar progress-bar-striped progress-bar-animated bg-gradient"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: @Percent%"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="steps d-flex justify-content-between mt-3 px-2">
|
||||||
|
@foreach (var step in Steps)
|
||||||
|
{
|
||||||
|
<div class="step text-center @(step.IsActive ? "active" : "") @(step.IsCompleted ? "completed" : "")">
|
||||||
|
<div class="step-circle mb-1">
|
||||||
|
@if (step.IsCompleted) { <span>✓</span> } else { <span>@step.Number</span> }
|
||||||
|
</div>
|
||||||
|
<small class="step-label d-none d-sm-block">@step.Label</small>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string Status { get; set; } = "Ready";
|
||||||
|
[Parameter] public int Percent { get; set; } = 0;
|
||||||
|
|
||||||
|
// Simple step model
|
||||||
|
public class Step
|
||||||
|
{
|
||||||
|
public int Number { get; set; }
|
||||||
|
public string Label { get; set; } = "";
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public bool IsCompleted { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can pass current step index from parent
|
||||||
|
[Parameter] public int CurrentStepIndex { get; set; } = 0;
|
||||||
|
|
||||||
|
private List<Step> Steps = new()
|
||||||
|
{
|
||||||
|
new Step { Number = 1, Label = "Download" },
|
||||||
|
new Step { Number = 2, Label = "Transcrição" },
|
||||||
|
new Step { Number = 3, Label = "Análise IA" },
|
||||||
|
new Step { Number = 4, Label = "PDF" }
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Steps.Count; i++)
|
||||||
|
{
|
||||||
|
Steps[i].IsCompleted = i < CurrentStepIndex;
|
||||||
|
Steps[i].IsActive = i == CurrentStepIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
VideoStudy.UI/Routes.razor
Normal file
6
VideoStudy.UI/Routes.razor
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<Router AppAssembly="typeof(Routes).Assembly">
|
||||||
|
<Found Context="routeData">
|
||||||
|
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||||
|
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||||
|
</Found>
|
||||||
|
</Router>
|
||||||
31
VideoStudy.UI/VideoStudy.UI.csproj
Normal file
31
VideoStudy.UI/VideoStudy.UI.csproj
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<SupportedPlatform Include="browser" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net8.0'">
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.22" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TargetFramework)' == 'net10.0'">
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\VideoStudy.Shared\VideoStudy.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
12
VideoStudy.UI/_Imports.razor
Normal file
12
VideoStudy.UI/_Imports.razor
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
@using System.Net.Http
|
||||||
|
@using System.Net.Http.Json
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
@using Microsoft.AspNetCore.Components.Web
|
||||||
|
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||||
|
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using VideoStudy.UI
|
||||||
|
@using VideoStudy.UI.Layout
|
||||||
|
@using VideoStudy.UI.Pages
|
||||||
|
@using VideoStudy.Shared
|
||||||
99
VideoStudy.UI/wwwroot/app.css
Normal file
99
VideoStudy.UI/wwwroot/app.css
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
:root {
|
||||||
|
/* Colors from vcart.me.novo analysis */
|
||||||
|
--primary: #007bff;
|
||||||
|
--secondary: #0056b3;
|
||||||
|
--accent-purple: #764ba2;
|
||||||
|
--accent-blue: #667eea;
|
||||||
|
--success: #28a745;
|
||||||
|
--text-color: #212529;
|
||||||
|
--bg-light: #f8f9fa;
|
||||||
|
--bg-white: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-light);
|
||||||
|
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Gradients */
|
||||||
|
.bg-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient {
|
||||||
|
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
|
||||||
|
border: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-gradient:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 15px rgba(118, 75, 162, 0.3);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.card {
|
||||||
|
border-radius: 15px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.processor-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accordion Custom */
|
||||||
|
.accordion-button:not(.collapsed) {
|
||||||
|
background-color: rgba(102, 126, 234, 0.1);
|
||||||
|
color: var(--accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-button:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: rgba(102, 126, 234, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Steps */
|
||||||
|
.step-circle {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e9ecef;
|
||||||
|
color: #6c757d;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.active .step-circle {
|
||||||
|
background: var(--primary);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 0 0 4px rgba(0, 123, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.step.completed .step-circle {
|
||||||
|
background: var(--success);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mode Selection */
|
||||||
|
.btn-outline-purple {
|
||||||
|
border: 2px solid var(--accent-purple);
|
||||||
|
color: var(--accent-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline-purple:hover, .btn-outline-purple.active {
|
||||||
|
background: var(--accent-purple);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utilities */
|
||||||
|
.shadow-soft {
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.08) !important;
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 18.2.11415.280
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Shared", "VideoStudy.Shared\VideoStudy.Shared.csproj", "{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Shared", "VideoStudy.Shared\VideoStudy.Shared.csproj", "{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}"
|
||||||
EndProject
|
EndProject
|
||||||
@ -13,34 +13,75 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Desktop", "Video
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Desktop.Client", "VideoStudy.Desktop\VideoStudy.Desktop.Client\VideoStudy.Desktop.Client.csproj", "{59672094-7BE6-4CB2-8401-59D59D8AF07A}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Desktop.Client", "VideoStudy.Desktop\VideoStudy.Desktop.Client\VideoStudy.Desktop.Client.csproj", "{59672094-7BE6-4CB2-8401-59D59D8AF07A}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.UI", "VideoStudy.UI\VideoStudy.UI.csproj", "{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VideoStudy.Native", "VideoStudy.Native\VideoStudy.Native.csproj", "{0387DAAC-724C-43D1-9C68-D8383F05E909}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
Release|x64 = Release|x64
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Debug|x64.Build.0 = Debug|x64
|
||||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{C2F5E2F7-0872-422B-B246-A9EC585AF3CC}.Release|x64.Build.0 = Release|x64
|
||||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Debug|x64.Build.0 = Debug|x64
|
||||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|Any CPU.Build.0 = Release|Any CPU
|
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{022CD193-2FB4-4507-BAA2-56DB7A40841E}.Release|x64.Build.0 = Release|x64
|
||||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC}.Release|x64.Build.0 = Release|x64
|
||||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A}.Release|x64.Build.0 = Release|x64
|
||||||
|
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{CE82389C-E484-4EAC-8F78-0FB5C6EB63A4}.Release|x64.Build.0 = Release|x64
|
||||||
|
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{0387DAAC-724C-43D1-9C68-D8383F05E909}.Release|x64.Build.0 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC} = {A245341F-91C7-4FC3-9EB8-FC6455180427}
|
{0E1E304A-DEC7-4704-BCE8-65A4DACE00BC} = {A245341F-91C7-4FC3-9EB8-FC6455180427}
|
||||||
{59672094-7BE6-4CB2-8401-59D59D8AF07A} = {A245341F-91C7-4FC3-9EB8-FC6455180427}
|
{59672094-7BE6-4CB2-8401-59D59D8AF07A} = {A245341F-91C7-4FC3-9EB8-FC6455180427}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {68B75065-924C-42D1-8B6C-A4B5678C2A85}
|
||||||
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user