generated from ricardo/MVCLogin
Compare commits
No commits in common. "4cca23cb35c65440ec5a2e10281f82d3646afba1" and "4b04639ad76330f538d67859f90c575d2e15a945" have entirely different histories.
4cca23cb35
...
4b04639ad7
@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net7.0</TargetFramework>
|
<TargetFramework>net7.0</TargetFramework>
|
||||||
|
|||||||
@ -6,6 +6,8 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace BaseDomain.Results
|
namespace BaseDomain.Results
|
||||||
{
|
{
|
||||||
|
public class Error
|
||||||
|
{
|
||||||
public enum ErrorTypeEnum
|
public enum ErrorTypeEnum
|
||||||
{
|
{
|
||||||
None = 0, //Erro vazio (para result)
|
None = 0, //Erro vazio (para result)
|
||||||
@ -14,8 +16,6 @@ namespace BaseDomain.Results
|
|||||||
Others = 3 //Inesperado ou sem categoria
|
Others = 3 //Inesperado ou sem categoria
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Error
|
|
||||||
{
|
|
||||||
public Error(ErrorTypeEnum error, string message, string description="")
|
public Error(ErrorTypeEnum error, string message, string description="")
|
||||||
{
|
{
|
||||||
ErrorType = error;
|
ErrorType = error;
|
||||||
|
|||||||
@ -10,7 +10,7 @@ namespace BaseDomain.Results
|
|||||||
{
|
{
|
||||||
protected Result(bool isSuccess, Error error)
|
protected Result(bool isSuccess, Error error)
|
||||||
{
|
{
|
||||||
if (isSuccess && error.ErrorType != ErrorTypeEnum.None)
|
if (isSuccess && error != Error.None)
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException();
|
throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
LogoPoost.png
BIN
LogoPoost.png
Binary file not shown.
|
Before Width: | Height: | Size: 107 KiB |
@ -11,8 +11,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Postall.Domain", "Postall.D
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BaseDomain", "BaseDomain\BaseDomain.csproj", "{72324CE0-8009-4876-9CF9-9F2BA8288B87}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BaseDomain", "BaseDomain\BaseDomain.csproj", "{72324CE0-8009-4876-9CF9-9F2BA8288B87}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Postall.Infra.MongoDB", "Postall.Infra.MongoDB\Postall.Infra.MongoDB.csproj", "{2EFC97F7-84D3-4582-9202-B03B401D9E9F}"
|
|
||||||
EndProject
|
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@ -35,10 +33,6 @@ Global
|
|||||||
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Release|Any CPU.Build.0 = Release|Any CPU
|
{72324CE0-8009-4876-9CF9-9F2BA8288B87}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{2EFC97F7-84D3-4582-9202-B03B401D9E9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{2EFC97F7-84D3-4582-9202-B03B401D9E9F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{2EFC97F7-84D3-4582-9202-B03B401D9E9F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{2EFC97F7-84D3-4582-9202-B03B401D9E9F}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
using BaseDomain.Results;
|
|
||||||
using Postall.Domain.Dtos;
|
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Application
|
|
||||||
{
|
|
||||||
public class ChannelApplicationService
|
|
||||||
{
|
|
||||||
private readonly IChannelService _channelService;
|
|
||||||
|
|
||||||
public ChannelApplicationService(IChannelService channelService)
|
|
||||||
{
|
|
||||||
_channelService = channelService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<Result<List<ChannelResponse>>> GetUserChannelsAsync()
|
|
||||||
{
|
|
||||||
return await _channelService.GetUserChannelsAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
using Postall.Domain.Entities;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Dtos
|
|
||||||
{
|
|
||||||
public class ChannelResponse
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string UserId { get; set; }
|
|
||||||
public string YoutubeId { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string ThumbnailUrl { get; set; }
|
|
||||||
public DateTime PublishedAt { get; set; }
|
|
||||||
public ulong SubscriberCount { get; set; }
|
|
||||||
public ulong VideoCount { get; set; }
|
|
||||||
|
|
||||||
// Propriedade para seleção de canais na interface
|
|
||||||
public bool IsSelected { get; set; }
|
|
||||||
|
|
||||||
// URL do canal no YouTube
|
|
||||||
public string ChannelUrl => $"https://www.youtube.com/channel/{Id}";
|
|
||||||
|
|
||||||
public ChannelData ToChannelData()
|
|
||||||
{
|
|
||||||
return new ChannelData
|
|
||||||
{
|
|
||||||
Id = Id,
|
|
||||||
UserId = UserId,
|
|
||||||
YoutubeId = YoutubeId,
|
|
||||||
Title = Title,
|
|
||||||
Description = Description,
|
|
||||||
ThumbnailUrl = ThumbnailUrl,
|
|
||||||
PublishedAt = PublishedAt,
|
|
||||||
SubscriberCount = SubscriberCount,
|
|
||||||
VideoCount = VideoCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
// Models/FacebookTokenResponse.cs
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Dtos.Responses
|
|
||||||
{
|
|
||||||
public class FacebookTokenResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("access_token")]
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("token_type")]
|
|
||||||
public string TokenType { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("expires_in")]
|
|
||||||
public long ExpiresIn { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Infra.Dtos
|
|
||||||
{
|
|
||||||
public class VideoItemResponse
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string ThumbnailUrl { get; set; }
|
|
||||||
public DateTime PublishedAt { get; set; }
|
|
||||||
public bool IsSelected { get; set; }
|
|
||||||
|
|
||||||
// Propriedades adicionais que podem ser úteis no futuro
|
|
||||||
public string ChannelTitle { get; set; }
|
|
||||||
public string ChannelId { get; set; }
|
|
||||||
public string VideoUrl => $"https://www.youtube.com/watch?v={Id}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
using MongoDB.Bson;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Postall.Domain.Dtos;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Entities
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Modelo de dados para armazenamento do canal no MongoDB
|
|
||||||
/// </summary>
|
|
||||||
public class ChannelData
|
|
||||||
{
|
|
||||||
[BsonId]
|
|
||||||
[BsonRepresentation(BsonType.String)]
|
|
||||||
public string Id { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("userId")]
|
|
||||||
public string UserId { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("channelId")]
|
|
||||||
public string ChannelId { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("youtubeId")]
|
|
||||||
public string YoutubeId { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("title")]
|
|
||||||
public string Title { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("description")]
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("thumbnailUrl")]
|
|
||||||
public string ThumbnailUrl { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("publishedAt")]
|
|
||||||
public DateTime PublishedAt { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("subscriberCount")]
|
|
||||||
public ulong SubscriberCount { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("videoCount")]
|
|
||||||
public ulong VideoCount { get; set; }
|
|
||||||
|
|
||||||
[BsonElement("isSelected")]
|
|
||||||
public bool IsSelected { get; set; }
|
|
||||||
|
|
||||||
[BsonIgnore]
|
|
||||||
public string ChannelUrl => $"https://www.youtube.com/channel/{YoutubeId}";
|
|
||||||
|
|
||||||
public ChannelResponse ToChannelResponse() => new ChannelResponse
|
|
||||||
{
|
|
||||||
Id = Id,
|
|
||||||
UserId = UserId,
|
|
||||||
YoutubeId = YoutubeId,
|
|
||||||
Title = Title,
|
|
||||||
Description = Description,
|
|
||||||
ThumbnailUrl = ThumbnailUrl,
|
|
||||||
PublishedAt = PublishedAt,
|
|
||||||
SubscriberCount = SubscriberCount,
|
|
||||||
VideoCount = VideoCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Entities
|
|
||||||
{
|
|
||||||
public class FacebookToken
|
|
||||||
{
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
public DateTime ExpiresAt { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
using MongoDB.Bson;
|
|
||||||
using MongoDB.Bson.Serialization.Attributes;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Entities
|
|
||||||
{
|
|
||||||
public class UserSocialData
|
|
||||||
{
|
|
||||||
[BsonId]
|
|
||||||
public ObjectId Id { get; set; }
|
|
||||||
public string UserId { get; set; }
|
|
||||||
public string GoogleToken { get; set; }
|
|
||||||
public FacebookToken FacebookToken { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
using Postall.Domain.Dtos;
|
|
||||||
using Postall.Domain.Entities;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Domain
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Interface para repositório de canais do YouTube no MongoDB
|
|
||||||
/// </summary>
|
|
||||||
public interface IChannelRepository
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém todos os canais
|
|
||||||
/// </summary>
|
|
||||||
Task<IEnumerable<ChannelData>> GetAllAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém um canal pelo ID do MongoDB
|
|
||||||
/// </summary>
|
|
||||||
Task<ChannelData> GetByIdAsync(string id);
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém os canais pelo ID do usuario do MongoDB
|
|
||||||
/// </summary>
|
|
||||||
Task<IList<ChannelData>> GetByUserIdAsync(string userId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém os canais pelo ID do usuario e o channelId
|
|
||||||
/// </summary>
|
|
||||||
Task<ChannelData> GetByUserIdAndChannelIdAsync(string userId, string channelId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém um canal pelo ID do YouTube
|
|
||||||
/// </summary>
|
|
||||||
Task<ChannelData> GetByYoutubeIdAsync(string youtubeId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adiciona um novo canal
|
|
||||||
/// </summary>
|
|
||||||
Task<ChannelData> AddAsync(ChannelData ChannelData);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adiciona vários canais de uma vez
|
|
||||||
/// </summary>
|
|
||||||
Task<IEnumerable<ChannelData>> AddManyAsync(IEnumerable<ChannelData> channels);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Atualiza um canal existente
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> UpdateAsync(ChannelData ChannelData);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove um canal pelo ID do MongoDB
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> DeleteAsync(string id);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove um canal pelo ID do YouTube
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> DeleteByYoutubeIdAsync(string youtubeId);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Busca canais com base em um termo de pesquisa no título ou descrição
|
|
||||||
/// </summary>
|
|
||||||
Task<IEnumerable<ChannelData>> SearchAsync(string searchTerm);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém canais selecionados (IsSelected = true)
|
|
||||||
/// </summary>
|
|
||||||
Task<IEnumerable<ChannelData>> GetSelectedAsync();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Marca ou desmarca um canal como selecionado
|
|
||||||
/// </summary>
|
|
||||||
Task<bool> SetSelectedStatusAsync(string id, bool isSelected);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converte ChannelResponse para ChannelData
|
|
||||||
/// </summary>
|
|
||||||
ChannelData ConvertFromResponse(ChannelResponse channelResponse);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converte ChannelData para ChannelResponse
|
|
||||||
/// </summary>
|
|
||||||
ChannelResponse ConvertToResponse(ChannelData ChannelData);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,9 +7,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="libc.eventbus" Version="7.1.2" />
|
|
||||||
<PackageReference Include="MongoDB.Bson" Version="3.1.0" />
|
|
||||||
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
|
|
||||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
|
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
|
||||||
@ -18,11 +15,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\BaseDomain\BaseDomain.csproj" />
|
<ProjectReference Include="..\..\vcart.me\vcart.back\Struct.ValueObjects\BaseDomain.csproj" />
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Events\" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,171 +0,0 @@
|
|||||||
using BaseDomain.Results;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Postall.Domain;
|
|
||||||
using Postall.Domain.Dtos;
|
|
||||||
using Postall.Domain.Entities;
|
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Postall.Infra.Services
|
|
||||||
{
|
|
||||||
public class ChannelVideoService: IChannelService
|
|
||||||
{
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
||||||
private readonly IChannelRepository _channelRepository;
|
|
||||||
private readonly IChannelYoutubeService _channelYoutubeService;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly string _apiKey;
|
|
||||||
|
|
||||||
public ChannelVideoService(
|
|
||||||
IHttpContextAccessor httpContextAccessor,
|
|
||||||
IChannelRepository channelRepository,
|
|
||||||
IChannelYoutubeService channelYoutubeService
|
|
||||||
)
|
|
||||||
{
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
|
||||||
this._channelRepository = channelRepository;
|
|
||||||
this._channelYoutubeService = channelYoutubeService;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém o ID do usuário atual a partir das claims
|
|
||||||
/// </summary>
|
|
||||||
private string GetCurrentUserId()
|
|
||||||
{
|
|
||||||
var user = _httpContextAccessor.HttpContext.User;
|
|
||||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
|
||||||
{
|
|
||||||
var email = user.FindFirst(ClaimTypes.Email)?.Value;
|
|
||||||
if (!string.IsNullOrEmpty(email))
|
|
||||||
{
|
|
||||||
userId = Convert.ToBase64String(
|
|
||||||
System.Text.Encoding.UTF8.GetBytes(email)
|
|
||||||
).Replace("/", "_").Replace("+", "-").Replace("=", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém a lista de canais que o usuário tem permissão para listar
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Result<List<ChannelResponse>>> GetUserChannelsAsync()
|
|
||||||
{
|
|
||||||
var userId = GetCurrentUserId();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
|
||||||
return new List<ChannelResponse>();
|
|
||||||
|
|
||||||
var channelList = await _channelRepository.GetByUserIdAsync(userId);
|
|
||||||
|
|
||||||
if (!channelList.Any())
|
|
||||||
{
|
|
||||||
var defaultChannels = new List<ChannelData>
|
|
||||||
{
|
|
||||||
new ChannelData { Id = "UC_x5XG1OV2P6uZZ5FSM9Ttw", Title = "Google Developers" },
|
|
||||||
new ChannelData { Id = "UCkw4JCwteGrDHIsyIIKo4tQ", Title = "Google" }
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var channel in defaultChannels)
|
|
||||||
{
|
|
||||||
var channelDetails = await this.GetChannelDetailsAsync(channel.Id);
|
|
||||||
if (channelDetails.IsSuccess)
|
|
||||||
{
|
|
||||||
channelList.Add(channelDetails.Value.ToChannelData());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Erro ao buscar detalhes do canal {channel.Id}: {channelDetails.Error.Description}");
|
|
||||||
channelList.Add(new ChannelData
|
|
||||||
{
|
|
||||||
Id = channel.Id,
|
|
||||||
Title = channel.Title,
|
|
||||||
ThumbnailUrl = "/images/default-channel-thumb.jpg" // Imagem padrão
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return channelList.Select(c => c.ToChannelResponse()).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adiciona um canal à lista do usuário
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Result<bool>> AddChannelAsync(string channelId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(channelId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var userId = GetCurrentUserId();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var channelItem = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (channelItem != null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var channelDetails = await GetChannelDetailsAsync(channelId);
|
|
||||||
|
|
||||||
await _channelRepository.AddAsync(channelDetails.Value.ToChannelData());
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Erro ao adicionar canal {channelId}: {ex.Message}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove um canal da lista do usuário
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Result<bool>> RemoveChannel(string channelId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(channelId))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var userId = GetCurrentUserId();
|
|
||||||
|
|
||||||
var channelItem = await _channelRepository.GetByUserIdAndChannelIdAsync(userId, channelId);
|
|
||||||
|
|
||||||
if (channelItem == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
await _channelRepository.DeleteAsync(channelId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Busca os detalhes de um canal na API do YouTube
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Result<ChannelResponse>> GetChannelDetailsAsync(string channelId)
|
|
||||||
{
|
|
||||||
var channel = await _channelYoutubeService.GetChannelDetailsAsync(channelId);
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Busca um canal pelo nome ou ID
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Result<List<ChannelResponse>>> SearchChannelsAsync(string query, int maxResults = 5)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(query) || query.Length < 3)
|
|
||||||
return new List<ChannelResponse>();
|
|
||||||
|
|
||||||
var results = await _channelYoutubeService.SearchChannelsAsync(query, maxResults);
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
using BaseDomain.Results;
|
|
||||||
using Postall.Domain.Dtos;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Services.Contracts
|
|
||||||
{
|
|
||||||
public interface IChannelService
|
|
||||||
{
|
|
||||||
Task<Result<List<ChannelResponse>>> GetUserChannelsAsync();
|
|
||||||
|
|
||||||
Task<Result<bool>> AddChannelAsync(string channelId);
|
|
||||||
|
|
||||||
Task<Result<bool>> RemoveChannel(string channelId);
|
|
||||||
|
|
||||||
Task<Result<List<ChannelResponse>>> SearchChannelsAsync(string query, int maxResults = 5);
|
|
||||||
|
|
||||||
Task<Result<ChannelResponse>> GetChannelDetailsAsync(string channelId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
using BaseDomain.Results;
|
|
||||||
using Postall.Domain.Dtos;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Services.Contracts
|
|
||||||
{
|
|
||||||
public interface IChannelYoutubeService
|
|
||||||
{
|
|
||||||
Task<Result<List<ChannelResponse>>> SearchChannelsAsync(string query, int maxResults = 5);
|
|
||||||
|
|
||||||
Task<Result<ChannelResponse>> GetChannelDetailsAsync(string channelId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Services.Contracts
|
|
||||||
{
|
|
||||||
public interface IFacebookServices
|
|
||||||
{
|
|
||||||
Task<string> GetLongLivedToken(string shortLivedToken);
|
|
||||||
Task SaveFacebookToken(string userId, string token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
using Postall.Infra.Dtos;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Domain.Services.Contracts
|
|
||||||
{
|
|
||||||
public interface IVideoService
|
|
||||||
{
|
|
||||||
Task<List<VideoItemResponse>> GetUserChannelVideosAsync(int maxResults = 10);
|
|
||||||
|
|
||||||
Task<List<VideoItemResponse>> GetChannelVideosAsync(string channelId, int maxResults = 10);
|
|
||||||
|
|
||||||
Task SaveVideoForUserAsync(string videoId);
|
|
||||||
|
|
||||||
Task RemoveVideoForUserAsync(string videoId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Postall.Domain;
|
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using Postall.Infra.MongoDB.Repositories;
|
|
||||||
using Postall.Infra.Services;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Infra.MongoDB.Extensions
|
|
||||||
{
|
|
||||||
public static class ServiceRepositoryExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddRepositories(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddScoped<IChannelRepository, ChannelRepository>();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,19 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0-preview.1.25080.5" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.0-preview.1.25080.5" />
|
|
||||||
<PackageReference Include="MongoDB.Driver" Version="3.2.1" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Postall.Domain\Postall.Domain.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@ -1,244 +0,0 @@
|
|||||||
using Microsoft.Extensions.Options;
|
|
||||||
using MongoDB.Bson;
|
|
||||||
using MongoDB.Driver;
|
|
||||||
using Postall.Domain;
|
|
||||||
using Postall.Domain.Dtos;
|
|
||||||
using Postall.Domain.Entities;
|
|
||||||
using Postall.Infra.MongoDB.Settings;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Channels;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
|
||||||
|
|
||||||
namespace Postall.Infra.MongoDB.Repositories
|
|
||||||
{
|
|
||||||
public class ChannelRepository : IChannelRepository
|
|
||||||
{
|
|
||||||
private readonly IMongoCollection<ChannelData> _channelsCollection;
|
|
||||||
|
|
||||||
public ChannelRepository(IOptions<MongoDbSettings> mongoDbSettings)
|
|
||||||
{
|
|
||||||
var client = new MongoClient(mongoDbSettings.Value.ConnectionString);
|
|
||||||
var database = client.GetDatabase(mongoDbSettings.Value.DatabaseName);
|
|
||||||
_channelsCollection = database.GetCollection<ChannelData>(mongoDbSettings.Value.ChannelsCollectionName);
|
|
||||||
|
|
||||||
var indexKeysDefinition = Builders<ChannelData>.IndexKeys.Ascending(c => c.YoutubeId);
|
|
||||||
_channelsCollection.Indexes.CreateOne(new CreateIndexModel<ChannelData>(indexKeysDefinition, new CreateIndexOptions { Unique = true }));
|
|
||||||
|
|
||||||
var textIndexDefinition = Builders<ChannelData>.IndexKeys
|
|
||||||
.Text(c => c.Title)
|
|
||||||
.Text(c => c.Description);
|
|
||||||
_channelsCollection.Indexes.CreateOne(new CreateIndexModel<ChannelData>(textIndexDefinition));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém todos os canais
|
|
||||||
/// </summary>
|
|
||||||
public async Task<IEnumerable<ChannelData>> GetAllAsync()
|
|
||||||
{
|
|
||||||
return await _channelsCollection.Find(c => true).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém um canal pelo ID do MongoDB
|
|
||||||
/// </summary>
|
|
||||||
public async Task<ChannelData> GetByIdAsync(string id)
|
|
||||||
{
|
|
||||||
if (!ObjectId.TryParse(id, out _))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return await _channelsCollection.Find(c => c.Id == id).FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IList<ChannelData>> GetByUserIdAsync(string userId)
|
|
||||||
{
|
|
||||||
return await _channelsCollection.Find(c => c.UserId == userId).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém os canais pelo ID do usuario e o channelId
|
|
||||||
/// </summary>
|
|
||||||
public async Task<ChannelData> GetByUserIdAndChannelIdAsync(string userId, string channelId)
|
|
||||||
{
|
|
||||||
return await _channelsCollection.Find(c => c.UserId == userId && c.ChannelId == channelId).FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém um canal pelo ID do YouTube
|
|
||||||
/// </summary>
|
|
||||||
public async Task<ChannelData> GetByYoutubeIdAsync(string youtubeId)
|
|
||||||
{
|
|
||||||
return await _channelsCollection.Find(c => c.YoutubeId == youtubeId).FirstOrDefaultAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adiciona um novo canal
|
|
||||||
/// </summary>
|
|
||||||
public async Task<ChannelData> AddAsync(ChannelData ChannelData)
|
|
||||||
{
|
|
||||||
// Verifica se o canal já existe pelo ID do YouTube
|
|
||||||
var existingChannel = await GetByYoutubeIdAsync(ChannelData.YoutubeId);
|
|
||||||
if (existingChannel != null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// Gera novo ID para o MongoDB se não for fornecido
|
|
||||||
if (string.IsNullOrEmpty(ChannelData.Id) || !ObjectId.TryParse(ChannelData.Id, out _))
|
|
||||||
{
|
|
||||||
ChannelData.Id = ObjectId.GenerateNewId().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
await _channelsCollection.InsertOneAsync(ChannelData);
|
|
||||||
return ChannelData;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adiciona vários canais de uma vez
|
|
||||||
/// </summary>
|
|
||||||
public async Task<IEnumerable<ChannelData>> AddManyAsync(IEnumerable<ChannelData> channels)
|
|
||||||
{
|
|
||||||
if (channels == null || !channels.Any())
|
|
||||||
return new List<ChannelData>();
|
|
||||||
|
|
||||||
var channelsList = channels.ToList();
|
|
||||||
|
|
||||||
// Gera novos IDs para o MongoDB se necessário
|
|
||||||
foreach (var ChannelData in channelsList)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(ChannelData.Id) || !ObjectId.TryParse(ChannelData.Id, out _))
|
|
||||||
{
|
|
||||||
ChannelData.Id = ObjectId.GenerateNewId().ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verifica canais existentes pelo ID do YouTube
|
|
||||||
var youtubeIds = channelsList.Select(c => c.YoutubeId).ToList();
|
|
||||||
var existingChannels = await _channelsCollection
|
|
||||||
.Find(c => youtubeIds.Contains(c.YoutubeId))
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
var existingYoutubeIds = existingChannels.Select(c => c.YoutubeId).ToHashSet();
|
|
||||||
var newChannels = channelsList.Where(c => !existingYoutubeIds.Contains(c.YoutubeId)).ToList();
|
|
||||||
|
|
||||||
if (newChannels.Any())
|
|
||||||
await _channelsCollection.InsertManyAsync(newChannels);
|
|
||||||
|
|
||||||
return newChannels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Atualiza um canal existente
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> UpdateAsync(ChannelData ChannelData)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(ChannelData.Id) || !ObjectId.TryParse(ChannelData.Id, out _))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var result = await _channelsCollection.ReplaceOneAsync(
|
|
||||||
c => c.Id == ChannelData.Id,
|
|
||||||
ChannelData,
|
|
||||||
new ReplaceOptions { IsUpsert = false });
|
|
||||||
|
|
||||||
return result.IsAcknowledged && result.ModifiedCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove um canal pelo ID do MongoDB
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> DeleteAsync(string id)
|
|
||||||
{
|
|
||||||
if (!ObjectId.TryParse(id, out _))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var result = await _channelsCollection.DeleteOneAsync(c => c.Id == id);
|
|
||||||
return result.IsAcknowledged && result.DeletedCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove um canal pelo ID do YouTube
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> DeleteByYoutubeIdAsync(string youtubeId)
|
|
||||||
{
|
|
||||||
var result = await _channelsCollection.DeleteOneAsync(c => c.YoutubeId == youtubeId);
|
|
||||||
return result.IsAcknowledged && result.DeletedCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Busca canais com base em um termo de pesquisa no título ou descrição
|
|
||||||
/// </summary>
|
|
||||||
public async Task<IEnumerable<ChannelData>> SearchAsync(string searchTerm)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(searchTerm))
|
|
||||||
return await GetAllAsync();
|
|
||||||
|
|
||||||
var filter = Builders<ChannelData>.Filter.Text(searchTerm);
|
|
||||||
return await _channelsCollection.Find(filter).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém canais selecionados (IsSelected = true)
|
|
||||||
/// </summary>
|
|
||||||
public async Task<IEnumerable<ChannelData>> GetSelectedAsync()
|
|
||||||
{
|
|
||||||
return await _channelsCollection.Find(c => c.IsSelected).ToListAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Marca ou desmarca um canal como selecionado
|
|
||||||
/// </summary>
|
|
||||||
public async Task<bool> SetSelectedStatusAsync(string id, bool isSelected)
|
|
||||||
{
|
|
||||||
if (!ObjectId.TryParse(id, out _))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var update = Builders<ChannelData>.Update.Set(c => c.IsSelected, isSelected);
|
|
||||||
var result = await _channelsCollection.UpdateOneAsync(c => c.Id == id, update);
|
|
||||||
|
|
||||||
return result.IsAcknowledged && result.ModifiedCount > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converte ChannelResponse para ChannelData
|
|
||||||
/// </summary>
|
|
||||||
public ChannelData ConvertFromResponse(ChannelResponse channelResponse)
|
|
||||||
{
|
|
||||||
if (channelResponse == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new ChannelData
|
|
||||||
{
|
|
||||||
YoutubeId = channelResponse.Id,
|
|
||||||
Title = channelResponse.Title,
|
|
||||||
Description = channelResponse.Description,
|
|
||||||
ThumbnailUrl = channelResponse.ThumbnailUrl,
|
|
||||||
PublishedAt = channelResponse.PublishedAt,
|
|
||||||
SubscriberCount = channelResponse.SubscriberCount,
|
|
||||||
VideoCount = channelResponse.VideoCount,
|
|
||||||
IsSelected = channelResponse.IsSelected
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converte ChannelData para ChannelResponse
|
|
||||||
/// </summary>
|
|
||||||
public ChannelResponse ConvertToResponse(ChannelData ChannelData)
|
|
||||||
{
|
|
||||||
if (ChannelData == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new ChannelResponse
|
|
||||||
{
|
|
||||||
Id = ChannelData.YoutubeId,
|
|
||||||
Title = ChannelData.Title,
|
|
||||||
Description = ChannelData.Description,
|
|
||||||
ThumbnailUrl = ChannelData.ThumbnailUrl,
|
|
||||||
PublishedAt = ChannelData.PublishedAt,
|
|
||||||
SubscriberCount = ChannelData.SubscriberCount,
|
|
||||||
VideoCount = ChannelData.VideoCount,
|
|
||||||
IsSelected = ChannelData.IsSelected
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Infra.MongoDB.Settings
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Configuração do MongoDB
|
|
||||||
/// </summary>
|
|
||||||
public class MongoDbSettings
|
|
||||||
{
|
|
||||||
public string ConnectionString { get; set; }
|
|
||||||
public string DatabaseName { get; set; }
|
|
||||||
public string ChannelsCollectionName { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Postall.Domain;
|
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using Postall.Infra.Services;
|
|
||||||
|
|
||||||
namespace BaseDomain.Extensions
|
|
||||||
{
|
|
||||||
public static class ServiceCollectionExtensions
|
|
||||||
{
|
|
||||||
public static IServiceCollection AddYouTubeServices(this IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddHttpContextAccessor();
|
|
||||||
|
|
||||||
services.AddScoped<IVideoService, YouTubeServiceVideo>();
|
|
||||||
|
|
||||||
services.AddScoped<IChannelService, ChannelVideoService>();
|
|
||||||
|
|
||||||
services.AddScoped<IChannelYoutubeService, ChannelYoutubeService>();
|
|
||||||
|
|
||||||
return services;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,9 +7,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Google.Apis.YouTube.v3" Version="1.69.0.3707" />
|
<PackageReference Include="MongoDB.Bson" Version="2.28.0" />
|
||||||
<PackageReference Include="MongoDB.Bson" Version="3.1.0" />
|
<PackageReference Include="MongoDB.Driver" Version="2.28.0" />
|
||||||
<PackageReference Include="MongoDB.Driver" Version="3.1.0" />
|
|
||||||
<PackageReference Include="Serilog" Version="4.0.2" />
|
<PackageReference Include="Serilog" Version="4.0.2" />
|
||||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||||
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
|
<PackageReference Include="Serilog.Enrichers.Context" Version="4.6.5" />
|
||||||
@ -17,8 +16,4 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
|
<PackageReference Include="Serilog.Sinks.Grafana.Loki" Version="8.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Postall.Domain\Postall.Domain.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,96 +0,0 @@
|
|||||||
using BaseDomain.Results;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Postall.Domain.Dtos;
|
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Infra.Services
|
|
||||||
{
|
|
||||||
public class ChannelYoutubeService: IChannelYoutubeService
|
|
||||||
{
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly string _apiKey;
|
|
||||||
|
|
||||||
public ChannelYoutubeService(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
_configuration = configuration;
|
|
||||||
_apiKey = _configuration["YouTube:ApiKey"];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Busca os detalhes de um canal na API do YouTube
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Result<ChannelResponse>> GetChannelDetailsAsync(string channelId)
|
|
||||||
{
|
|
||||||
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
|
||||||
|
|
||||||
var channelsRequest = youtubeService.Channels.List("snippet,statistics,contentDetails");
|
|
||||||
channelsRequest.Id = channelId;
|
|
||||||
|
|
||||||
var response = await channelsRequest.ExecuteAsync();
|
|
||||||
|
|
||||||
if (response.Items.Count == 0)
|
|
||||||
throw new Exception($"Canal não encontrado: {channelId}");
|
|
||||||
|
|
||||||
var channel = response.Items[0];
|
|
||||||
|
|
||||||
return new ChannelResponse
|
|
||||||
{
|
|
||||||
Id = channel.Id,
|
|
||||||
Title = channel.Snippet.Title,
|
|
||||||
Description = channel.Snippet.Description,
|
|
||||||
ThumbnailUrl = channel.Snippet.Thumbnails.Default__.Url,
|
|
||||||
PublishedAt = channel.Snippet.PublishedAt.Value,
|
|
||||||
SubscriberCount = channel.Statistics.SubscriberCount.GetValueOrDefault(),
|
|
||||||
VideoCount = channel.Statistics.VideoCount.GetValueOrDefault()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Busca um canal pelo nome ou ID
|
|
||||||
/// </summary>
|
|
||||||
public async Task<Result<List<ChannelResponse>>> SearchChannelsAsync(string query, int maxResults = 5)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(query) || query.Length < 3)
|
|
||||||
return new List<ChannelResponse>();
|
|
||||||
|
|
||||||
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
|
||||||
|
|
||||||
var searchRequest = youtubeService.Search.List("snippet");
|
|
||||||
searchRequest.Q = query;
|
|
||||||
searchRequest.Type = "channel";
|
|
||||||
searchRequest.MaxResults = maxResults;
|
|
||||||
|
|
||||||
var searchResponse = await searchRequest.ExecuteAsync();
|
|
||||||
|
|
||||||
var results = new List<ChannelResponse>();
|
|
||||||
|
|
||||||
foreach (var item in searchResponse.Items)
|
|
||||||
{
|
|
||||||
results.Add(new ChannelResponse
|
|
||||||
{
|
|
||||||
Id = item.Id.ChannelId,
|
|
||||||
Title = item.Snippet.Title,
|
|
||||||
Description = item.Snippet.Description,
|
|
||||||
ThumbnailUrl = item.Snippet.Thumbnails.Default__.Url,
|
|
||||||
PublishedAt = item.Snippet.PublishedAt.Value
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Classe auxiliar para dados básicos de canal
|
|
||||||
public class ChannelData
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using MongoDB.Driver;
|
|
||||||
using Postall.Domain.Dtos.Responses;
|
|
||||||
using Postall.Domain.Entities;
|
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http.Json;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Postall.Infra.Services
|
|
||||||
{
|
|
||||||
public class FacebookTokenService: IFacebookServices
|
|
||||||
{
|
|
||||||
private readonly IMongoCollection<UserSocialData> _tokens;
|
|
||||||
private readonly IConfiguration _config;
|
|
||||||
private readonly HttpClient _httpClient;
|
|
||||||
|
|
||||||
public async Task<string> GetLongLivedToken(string shortLivedToken)
|
|
||||||
{
|
|
||||||
var appId = _config["Authentication:Facebook:AppId"];
|
|
||||||
var appSecret = _config["Authentication:Facebook:AppSecret"];
|
|
||||||
|
|
||||||
var response = await _httpClient.GetFromJsonAsync<FacebookTokenResponse>(
|
|
||||||
$"https://graph.facebook.com/oauth/access_token?" +
|
|
||||||
$"grant_type=fb_exchange_token&" +
|
|
||||||
$"client_id={appId}&" +
|
|
||||||
$"client_secret={appSecret}&" +
|
|
||||||
$"fb_exchange_token={shortLivedToken}");
|
|
||||||
|
|
||||||
return response.AccessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task SaveFacebookToken(string userId, string token)
|
|
||||||
{
|
|
||||||
var update = Builders<UserSocialData>.Update
|
|
||||||
.Set(x => x.FacebookToken.AccessToken, token)
|
|
||||||
.Set(x => x.FacebookToken.ExpiresAt, DateTime.UtcNow.AddDays(60));
|
|
||||||
|
|
||||||
await _tokens.UpdateOneAsync(
|
|
||||||
x => x.UserId == userId,
|
|
||||||
update,
|
|
||||||
new UpdateOptions { IsUpsert = true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,227 +0,0 @@
|
|||||||
using Google.Apis.Services;
|
|
||||||
using Google.Apis.YouTube.v3;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using Postall.Infra.Dtos;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Postall.Infra.Services
|
|
||||||
{
|
|
||||||
public class YouTubeServiceVideo: IVideoService
|
|
||||||
{
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
||||||
private readonly string _apiKey;
|
|
||||||
private readonly string _clientId;
|
|
||||||
private readonly string _clientSecret;
|
|
||||||
|
|
||||||
public YouTubeServiceVideo(IConfiguration configuration, IHttpContextAccessor httpContextAccessor)
|
|
||||||
{
|
|
||||||
_configuration = configuration;
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
|
||||||
|
|
||||||
_apiKey = _configuration["YouTube:ApiKey"];
|
|
||||||
_clientId = _configuration["Authentication:Google:ClientId"];
|
|
||||||
_clientSecret = _configuration["Authentication:Google:ClientSecret"];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém o ID do usuário atual a partir das claims
|
|
||||||
/// </summary>
|
|
||||||
private string GetCurrentUserId()
|
|
||||||
{
|
|
||||||
var user = _httpContextAccessor.HttpContext.User;
|
|
||||||
|
|
||||||
// Primeiro tentar obter o NameIdentifier (que foi adicionado no ExternalLoginCallback)
|
|
||||||
var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
|
|
||||||
// Se não encontrou, tenta usar o email como identificador
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
|
||||||
{
|
|
||||||
var email = user.FindFirst(ClaimTypes.Email)?.Value;
|
|
||||||
if (!string.IsNullOrEmpty(email))
|
|
||||||
{
|
|
||||||
// Gerar ID baseado no email (mesmo método usado no ExternalLoginCallback)
|
|
||||||
userId = Convert.ToBase64String(
|
|
||||||
System.Text.Encoding.UTF8.GetBytes(email)
|
|
||||||
).Replace("/", "_").Replace("+", "-").Replace("=", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém os vídeos dos canais que o usuário tem acesso, ordenados por data
|
|
||||||
/// </summary>
|
|
||||||
public async Task<List<VideoItemResponse>> GetUserChannelVideosAsync(int maxResults = 10)
|
|
||||||
{
|
|
||||||
// Obter o ID do usuário das claims
|
|
||||||
var userId = GetCurrentUserId();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
|
||||||
return new List<VideoItemResponse>();
|
|
||||||
|
|
||||||
// Obter os canais do usuário do banco de dados
|
|
||||||
var userChannels = await GetUserChannelsAsync(userId);
|
|
||||||
|
|
||||||
if (!userChannels.Any())
|
|
||||||
return new List<VideoItemResponse>();
|
|
||||||
|
|
||||||
var videos = new List<VideoItemResponse>();
|
|
||||||
|
|
||||||
// Para cada canal, buscar os vídeos recentes
|
|
||||||
foreach (var channelId in userChannels)
|
|
||||||
{
|
|
||||||
var channelVideos = await GetChannelVideosAsync(channelId, maxResults);
|
|
||||||
videos.AddRange(channelVideos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ordenar por data de publicação e pegar os mais recentes
|
|
||||||
return videos
|
|
||||||
.OrderByDescending(v => v.PublishedAt)
|
|
||||||
.Take(maxResults)
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Obtém os vídeos de um canal específico
|
|
||||||
/// </summary>
|
|
||||||
public async Task<List<VideoItemResponse>> GetChannelVideosAsync(string channelId, int maxResults = 10)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var youtubeService = new YouTubeBaseServiceWrapper(_apiKey);
|
|
||||||
|
|
||||||
// Primeiro, buscar os IDs dos últimos vídeos do canal
|
|
||||||
var searchRequest = youtubeService.Search.List("snippet");
|
|
||||||
searchRequest.ChannelId = channelId;
|
|
||||||
searchRequest.Order = SearchResource.ListRequest.OrderEnum.Date;
|
|
||||||
searchRequest.MaxResults = maxResults;
|
|
||||||
searchRequest.Type = "video";
|
|
||||||
|
|
||||||
var searchResponse = await searchRequest.ExecuteAsync();
|
|
||||||
|
|
||||||
if (searchResponse.Items == null || searchResponse.Items.Count == 0)
|
|
||||||
return new List<VideoItemResponse>();
|
|
||||||
|
|
||||||
// Obter os IDs dos vídeos
|
|
||||||
var videoIds = searchResponse.Items.Select(i => i.Id.VideoId).ToList();
|
|
||||||
|
|
||||||
// Buscar detalhes dos vídeos
|
|
||||||
var videosRequest = youtubeService.Videos.List("snippet,contentDetails,statistics");
|
|
||||||
videosRequest.Id = string.Join(",", videoIds);
|
|
||||||
var videosResponse = await videosRequest.ExecuteAsync();
|
|
||||||
|
|
||||||
// Converter para o modelo
|
|
||||||
var videos = new List<VideoItemResponse>();
|
|
||||||
foreach (var videoItem in videosResponse.Items)
|
|
||||||
{
|
|
||||||
var searchItem = searchResponse.Items.FirstOrDefault(i => i.Id.VideoId == videoItem.Id);
|
|
||||||
|
|
||||||
videos.Add(new VideoItemResponse
|
|
||||||
{
|
|
||||||
Id = videoItem.Id,
|
|
||||||
Title = videoItem.Snippet.Title,
|
|
||||||
Description = videoItem.Snippet.Description,
|
|
||||||
ThumbnailUrl = videoItem.Snippet.Thumbnails.High.Url,
|
|
||||||
PublishedAt = videoItem.Snippet.PublishedAt.Value,
|
|
||||||
ChannelId = videoItem.Snippet.ChannelId,
|
|
||||||
ChannelTitle = videoItem.Snippet.ChannelTitle
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return videos;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Em produção, use um logger adequado
|
|
||||||
Console.WriteLine($"Erro ao obter vídeos do canal {channelId}: {ex.Message}");
|
|
||||||
return new List<VideoItemResponse>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Método para buscar os canais que o usuário gerencia ou tem acesso
|
|
||||||
/// Em um cenário real, isso viria do banco de dados
|
|
||||||
/// </summary>
|
|
||||||
private async Task<List<string>> GetUserChannelsAsync(string userId)
|
|
||||||
{
|
|
||||||
// TODO: Substituir isso por uma consulta real ao banco de dados
|
|
||||||
|
|
||||||
// Em produção, você deve implementar uma tabela que relacione
|
|
||||||
// os usuários com seus canais do YouTube
|
|
||||||
|
|
||||||
// Para teste, aqui estão alguns canais populares
|
|
||||||
// Na implementação real, você deve armazenar os canais que o usuário tem permissão
|
|
||||||
return new List<string>
|
|
||||||
{
|
|
||||||
"UC_x5XG1OV2P6uZZ5FSM9Ttw", // Google Developers
|
|
||||||
"UCt84aUC9OG6di8kSdKzEHTQ", // Outro canal de exemplo
|
|
||||||
"UCkw4JCwteGrDHIsyIIKo4tQ" // Google
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Salva um vídeo para o usuário atual
|
|
||||||
/// Em produção, isso salvaria no banco de dados
|
|
||||||
/// </summary>
|
|
||||||
public async Task SaveVideoForUserAsync(string videoId)
|
|
||||||
{
|
|
||||||
var userId = GetCurrentUserId();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(videoId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// TODO: Salvar no banco de dados a relação entre o usuário e o vídeo
|
|
||||||
|
|
||||||
// Código de exemplo:
|
|
||||||
// await _dbContext.UserVideos.AddAsync(new UserVideo
|
|
||||||
// {
|
|
||||||
// UserId = userId,
|
|
||||||
// VideoId = videoId,
|
|
||||||
// DateAdded = DateTime.UtcNow
|
|
||||||
// });
|
|
||||||
// await _dbContext.SaveChangesAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Remove um vídeo do usuário atual
|
|
||||||
/// Em produção, isso removeria do banco de dados
|
|
||||||
/// </summary>
|
|
||||||
public async Task RemoveVideoForUserAsync(string videoId)
|
|
||||||
{
|
|
||||||
var userId = GetCurrentUserId();
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(videoId))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// TODO: Remover do banco de dados a relação entre o usuário e o vídeo
|
|
||||||
|
|
||||||
// Código de exemplo:
|
|
||||||
// var userVideo = await _dbContext.UserVideos
|
|
||||||
// .FirstOrDefaultAsync(uv => uv.UserId == userId && uv.VideoId == videoId);
|
|
||||||
// if (userVideo != null)
|
|
||||||
// {
|
|
||||||
// _dbContext.UserVideos.Remove(userVideo);
|
|
||||||
// await _dbContext.SaveChangesAsync();
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Wrapper para o serviço base do YouTube
|
|
||||||
/// </summary>
|
|
||||||
public class YouTubeBaseServiceWrapper : YouTubeService
|
|
||||||
{
|
|
||||||
public YouTubeBaseServiceWrapper(string apiKey)
|
|
||||||
: base(new BaseClientService.Initializer()
|
|
||||||
{
|
|
||||||
ApiKey = apiKey,
|
|
||||||
ApplicationName = "PostAll"
|
|
||||||
})
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using Postall.Infra.Services;
|
|
||||||
|
|
||||||
namespace Postall.Controllers
|
|
||||||
{
|
|
||||||
[Authorize]
|
|
||||||
public class ChannelsController : Controller
|
|
||||||
{
|
|
||||||
private readonly IChannelService _channelService;
|
|
||||||
|
|
||||||
public ChannelsController(IChannelService channelService)
|
|
||||||
{
|
|
||||||
_channelService = channelService;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<IActionResult> Index()
|
|
||||||
{
|
|
||||||
var userChannels = await _channelService.GetUserChannelsAsync();
|
|
||||||
return View(userChannels);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult Add()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> Add(string channelId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(channelId))
|
|
||||||
{
|
|
||||||
TempData["Error"] = "ID do canal é obrigatório";
|
|
||||||
return RedirectToAction("Add");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var success = await _channelService.AddChannelAsync(channelId);
|
|
||||||
|
|
||||||
if (success.IsSuccess)
|
|
||||||
{
|
|
||||||
TempData["Message"] = "Canal adicionado com sucesso!";
|
|
||||||
return RedirectToAction("Index");
|
|
||||||
}
|
|
||||||
|
|
||||||
TempData["Error"] = "Não foi possível adicionar o canal. Ele já existe ou é inválido.";
|
|
||||||
return RedirectToAction("Add");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
TempData["Error"] = $"Erro ao adicionar canal: {ex.Message}";
|
|
||||||
return RedirectToAction("Add");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> Remove(string id)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var success = await _channelService.RemoveChannel(id);
|
|
||||||
|
|
||||||
if (success.IsSuccess)
|
|
||||||
TempData["Message"] = "Canal removido com sucesso!";
|
|
||||||
else
|
|
||||||
TempData["Error"] = "Não foi possível remover o canal.";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
TempData["Error"] = $"Erro ao remover canal: {ex.Message}";
|
|
||||||
}
|
|
||||||
|
|
||||||
return RedirectToAction("Index");
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public async Task<IActionResult> Search(string query)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(query) || query.Length < 3)
|
|
||||||
{
|
|
||||||
return Json(new { success = false, message = "A consulta deve ter pelo menos 3 caracteres" });
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var results = await _channelService.SearchChannelsAsync(query);
|
|
||||||
return Json(new { success = true, data = results });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Json(new { success = false, message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -46,29 +46,18 @@ namespace Postall.Controllers
|
|||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<ActionResult> ExternalLoginCallback(string code = "")
|
public async Task<ActionResult> ExternalLoginCallback(string code="")
|
||||||
{
|
{
|
||||||
|
//TODO: Temporário
|
||||||
var emailExist = HttpContext.User.FindFirst(ClaimTypes.Email).Value;
|
var emailExist = HttpContext.User.FindFirst(ClaimTypes.Email).Value;
|
||||||
if (emailExist != null)
|
if (emailExist != null)
|
||||||
{
|
{
|
||||||
var uniqueId = Convert.ToBase64String(
|
|
||||||
System.Text.Encoding.UTF8.GetBytes(emailExist)
|
|
||||||
).Replace("/", "_").Replace("+", "-").Replace("=", "");
|
|
||||||
|
|
||||||
var claims = new List<Claim>
|
var claims = new List<Claim>
|
||||||
{
|
{
|
||||||
// Adicionando o NameIdentifier que serve como userId
|
|
||||||
new Claim(ClaimTypes.NameIdentifier, uniqueId),
|
|
||||||
|
|
||||||
// Claims existentes
|
|
||||||
new Claim(ClaimTypes.Name, emailExist),
|
new Claim(ClaimTypes.Name, emailExist),
|
||||||
new Claim(ClaimTypes.Email, emailExist), // Garantindo que o email esteja nas claims
|
|
||||||
new Claim("FirstName", HttpContext.User.FindFirst(ClaimTypes.GivenName).Value),
|
new Claim("FirstName", HttpContext.User.FindFirst(ClaimTypes.GivenName).Value),
|
||||||
new Claim("FullName", HttpContext.User.FindFirst(ClaimTypes.GivenName).Value + " " + HttpContext.User.FindFirst(ClaimTypes.Surname).Value),
|
new Claim("FullName", HttpContext.User.FindFirst(ClaimTypes.GivenName).Value + " " + HttpContext.User.FindFirst(ClaimTypes.Surname).Value),
|
||||||
new Claim(ClaimTypes.Role, "User"),
|
new Claim(ClaimTypes.Role, "User"),
|
||||||
|
|
||||||
// Opcionalmente, adicionar informações do Google que serão úteis para o YouTube
|
|
||||||
new Claim("GoogleAccount", "true")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var claimsIdentity = new ClaimsIdentity(
|
var claimsIdentity = new ClaimsIdentity(
|
||||||
@ -80,17 +69,21 @@ namespace Postall.Controllers
|
|||||||
{
|
{
|
||||||
//AllowRefresh = <bool>,
|
//AllowRefresh = <bool>,
|
||||||
// Refreshing the authentication session should be allowed.
|
// Refreshing the authentication session should be allowed.
|
||||||
|
|
||||||
//ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
|
//ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
|
||||||
// The time at which the authentication ticket expires. A
|
// The time at which the authentication ticket expires. A
|
||||||
// value set here overrides the ExpireTimeSpan option of
|
// value set here overrides the ExpireTimeSpan option of
|
||||||
// CookieAuthenticationOptions set with AddCookie.
|
// CookieAuthenticationOptions set with AddCookie.
|
||||||
|
|
||||||
//IsPersistent = true,
|
//IsPersistent = true,
|
||||||
// Whether the authentication session is persisted across
|
// Whether the authentication session is persisted across
|
||||||
// multiple requests. When used with cookies, controls
|
// multiple requests. When used with cookies, controls
|
||||||
// whether the cookie's lifetime is absolute (matching the
|
// whether the cookie's lifetime is absolute (matching the
|
||||||
// lifetime of the authentication ticket) or session-based.
|
// lifetime of the authentication ticket) or session-based.
|
||||||
|
|
||||||
//IssuedUtc = <DateTimeOffset>,
|
//IssuedUtc = <DateTimeOffset>,
|
||||||
// The time at which the authentication ticket was issued.
|
// The time at which the authentication ticket was issued.
|
||||||
|
|
||||||
//RedirectUri = <string>
|
//RedirectUri = <string>
|
||||||
// The full path or absolute URI to be used as an http
|
// The full path or absolute URI to be used as an http
|
||||||
// redirect response value.
|
// redirect response value.
|
||||||
@ -103,10 +96,10 @@ namespace Postall.Controllers
|
|||||||
|
|
||||||
return RedirectToAction("Index", "Startup");
|
return RedirectToAction("Index", "Startup");
|
||||||
}
|
}
|
||||||
|
|
||||||
ViewBag.ErrorTitle = $"Email claim not received from: Microsoft";
|
ViewBag.ErrorTitle = $"Email claim not received from: Microsoft";
|
||||||
ViewBag.ErrorMessage = "Please contact support on info@dotnettutorials.net";
|
ViewBag.ErrorMessage = "Please contact support on info@dotnettutorials.net";
|
||||||
return View("Error");
|
return View("Error");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
|||||||
@ -2,19 +2,11 @@
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using Microsoft.AspNetCore.Authentication.Facebook;
|
using Microsoft.AspNetCore.Authentication.Facebook;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using System.Security.Claims;
|
|
||||||
|
|
||||||
namespace Postall.Controllers
|
namespace Postall.Controllers
|
||||||
{
|
{
|
||||||
public class OtherLoginsController : Controller
|
public class OtherLoginsController : Controller
|
||||||
{
|
{
|
||||||
private readonly IFacebookServices _facebookServices;
|
|
||||||
|
|
||||||
public OtherLoginsController(IFacebookServices facebookServices)
|
|
||||||
{
|
|
||||||
this._facebookServices = facebookServices;
|
|
||||||
}
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public IActionResult Index()
|
public IActionResult Index()
|
||||||
{
|
{
|
||||||
@ -32,13 +24,21 @@ namespace Postall.Controllers
|
|||||||
public async Task<IActionResult> FacebookResponse()
|
public async Task<IActionResult> FacebookResponse()
|
||||||
{
|
{
|
||||||
var result = await HttpContext.AuthenticateAsync(FacebookDefaults.AuthenticationScheme);
|
var result = await HttpContext.AuthenticateAsync(FacebookDefaults.AuthenticationScheme);
|
||||||
if (!result.Succeeded) return RedirectToAction("Login");
|
|
||||||
|
|
||||||
var accessToken = result.Properties.GetTokenValue("access_token");
|
if (!result.Succeeded)
|
||||||
var longLivedToken = await _facebookServices.GetLongLivedToken(accessToken);
|
return RedirectToAction("Login");
|
||||||
|
|
||||||
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
var claims = result.Principal.Identities.FirstOrDefault()
|
||||||
await _facebookServices.SaveFacebookToken(userId, longLivedToken);
|
.Claims.Select(claim => new
|
||||||
|
{
|
||||||
|
claim.Issuer,
|
||||||
|
claim.OriginalIssuer,
|
||||||
|
claim.Type,
|
||||||
|
claim.Value
|
||||||
|
});
|
||||||
|
|
||||||
|
// Aqui você pode implementar sua lógica de login
|
||||||
|
// Por exemplo, criar ou atualizar o usuário no banco de dados
|
||||||
|
|
||||||
return RedirectToAction("Index", "Home");
|
return RedirectToAction("Index", "Home");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Postall.Models;
|
|
||||||
|
|
||||||
namespace Postall.Controllers
|
|
||||||
{
|
|
||||||
public class SocialMediaController : Controller
|
|
||||||
{
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
// Implementar lógica para buscar lista de posts
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public JsonResult GetPostDetails(int postId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Implementar lógica para buscar detalhes do post
|
|
||||||
return Json(new { success = true });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Json(new { success = false, message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public JsonResult GetSocialMediaStatus(int postId)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Implementar lógica para buscar status das redes sociais
|
|
||||||
return Json(new { success = true });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Json(new { success = false, message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult Post()
|
|
||||||
{
|
|
||||||
return View();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public JsonResult SaveDraft([FromBody] PostViewModel model)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Lógica para salvar rascunho
|
|
||||||
return Json(new { success = true });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Json(new { success = false, message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public JsonResult PublishPost([FromBody] PostViewModel model)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Lógica para publicar post
|
|
||||||
return Json(new { success = true });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Json(new { success = false, message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public JsonResult SchedulePost([FromBody] PostScheduleViewModel model)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Lógica para agendar post
|
|
||||||
return Json(new { success = true });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Json(new { success = false, message = ex.Message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,75 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Postall.Models;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using Postall.Models;
|
|
||||||
|
|
||||||
namespace Postall.Controllers
|
|
||||||
{
|
|
||||||
[Authorize]
|
|
||||||
public class VideosController : Controller
|
|
||||||
{
|
|
||||||
// Simulação de dados para exemplo - em produção, estes dados viriam de uma API ou banco de dados
|
|
||||||
private List<VideoViewModel> GetSampleVideos()
|
|
||||||
{
|
|
||||||
return new List<VideoViewModel>
|
|
||||||
{
|
|
||||||
new VideoViewModel
|
|
||||||
{
|
|
||||||
Id = "video1",
|
|
||||||
Title = "Como usar o PostAll - Tutorial",
|
|
||||||
Description = "Aprenda a usar todas as funcionalidades do PostAll para gerenciar suas redes sociais.",
|
|
||||||
ThumbnailUrl = "https://i.ytimg.com/vi/sample1/maxresdefault.jpg",
|
|
||||||
PublishedAt = DateTime.Now.AddDays(-2)
|
|
||||||
},
|
|
||||||
new VideoViewModel
|
|
||||||
{
|
|
||||||
Id = "video2",
|
|
||||||
Title = "Estratégias de Marketing Digital para 2024",
|
|
||||||
Description = "Conheça as melhores estratégias para alavancar seu negócio nas redes sociais em 2024.",
|
|
||||||
ThumbnailUrl = "https://i.ytimg.com/vi/sample2/maxresdefault.jpg",
|
|
||||||
PublishedAt = DateTime.Now.AddDays(-5)
|
|
||||||
},
|
|
||||||
new VideoViewModel
|
|
||||||
{
|
|
||||||
Id = "video3",
|
|
||||||
Title = "Análise de Métricas nas Redes Sociais",
|
|
||||||
Description = "Aprenda a interpretar as métricas das suas redes sociais e tomar decisões baseadas em dados.",
|
|
||||||
ThumbnailUrl = "https://i.ytimg.com/vi/sample3/maxresdefault.jpg",
|
|
||||||
PublishedAt = DateTime.Now.AddDays(-7)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public IActionResult Index()
|
|
||||||
{
|
|
||||||
var userVideos = GetSampleVideos();
|
|
||||||
return View(userVideos);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult GetChannelVideos()
|
|
||||||
{
|
|
||||||
// Em um cenário real, você buscaria os vídeos da API do YouTube
|
|
||||||
var channelVideos = GetSampleVideos().OrderByDescending(v => v.PublishedAt).Take(10).ToList();
|
|
||||||
return PartialView("_ChannelVideosPartial", channelVideos);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public IActionResult AddVideos(string[] selectedVideos)
|
|
||||||
{
|
|
||||||
if (selectedVideos == null || selectedVideos.Length == 0)
|
|
||||||
{
|
|
||||||
return RedirectToAction("Index");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Em um cenário real, você salvaria esses IDs no banco de dados
|
|
||||||
// Aqui apenas redirecionamos de volta para o Index
|
|
||||||
|
|
||||||
TempData["Message"] = $"{selectedVideos.Length} vídeo(s) adicionado(s) com sucesso!";
|
|
||||||
return RedirectToAction("Index");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
namespace Postall.Models
|
|
||||||
{
|
|
||||||
public class PostListViewModel
|
|
||||||
{
|
|
||||||
public int Id { get; set; }
|
|
||||||
public string Channel { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public DateTime? NextScheduledDate { get; set; }
|
|
||||||
public DateTime LastUpdate { get; set; }
|
|
||||||
public List<SocialMediaStatusViewModel> SocialMediaStatus { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
namespace Postall.Models
|
|
||||||
{
|
|
||||||
public class PostScheduleViewModel
|
|
||||||
{
|
|
||||||
public bool IsManual { get; set; }
|
|
||||||
public DayOfWeek? WeekDay { get; set; }
|
|
||||||
public TimeSpan? Time { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
namespace Postall.Models
|
|
||||||
{
|
|
||||||
public class PostViewModel
|
|
||||||
{
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Content { get; set; }
|
|
||||||
public string ImageUrl { get; set; }
|
|
||||||
public List<string> SelectedPlatforms { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
namespace Postall.Models
|
|
||||||
{
|
|
||||||
public class SocialMediaStatusViewModel
|
|
||||||
{
|
|
||||||
public string Platform { get; set; }
|
|
||||||
public DateTime? NextScheduledDate { get; set; }
|
|
||||||
public string Status { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Postall.Models
|
|
||||||
{
|
|
||||||
|
|
||||||
public class VideoViewModel
|
|
||||||
{
|
|
||||||
public string Id { get; set; }
|
|
||||||
public string Title { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string ThumbnailUrl { get; set; }
|
|
||||||
public DateTime PublishedAt { get; set; }
|
|
||||||
public bool IsSelected { get; set; }
|
|
||||||
|
|
||||||
// Propriedades adicionais que podem ser úteis no futuro
|
|
||||||
public string ChannelTitle { get; set; }
|
|
||||||
public string ChannelId { get; set; }
|
|
||||||
public string VideoUrl => $"https://www.youtube.com/watch?v={Id}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -32,12 +32,6 @@
|
|||||||
<Folder Include="wwwroot\img\" />
|
<Folder Include="wwwroot\img\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Postall.Domain\Postall.Domain.csproj" />
|
|
||||||
<ProjectReference Include="..\Postall.Infra.MongoDB\Postall.Infra.MongoDB.csproj" />
|
|
||||||
<ProjectReference Include="..\Postall.Infra\Postall.Infra.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Resource.Designer.cs">
|
<Compile Update="Resource.Designer.cs">
|
||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
|
|||||||
@ -1,20 +1,15 @@
|
|||||||
using BaseDomain.Extensions;
|
|
||||||
using Blinks.LogConfig;
|
using Blinks.LogConfig;
|
||||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authentication.Google;
|
using Microsoft.AspNetCore.Authentication.Google;
|
||||||
using Microsoft.AspNetCore.Localization;
|
using Microsoft.AspNetCore.Localization;
|
||||||
using Microsoft.AspNetCore.Mvc.Razor;
|
using Microsoft.AspNetCore.Mvc.Razor;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Postall.Domain.Services.Contracts;
|
|
||||||
using Postall.Infra.Services;
|
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Serilog.Sinks.Grafana.Loki;
|
using Serilog.Sinks.Grafana.Loki;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using Stripe.Forwarding;
|
using Stripe.Forwarding;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Security.Policy;
|
using System.Security.Policy;
|
||||||
using Postall.Infra.MongoDB.Extensions;
|
|
||||||
using Postall.Infra.MongoDB.Settings;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@ -91,15 +86,6 @@ builder.Services.AddControllersWithViews();
|
|||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
builder.Services.AddSerilog();
|
builder.Services.AddSerilog();
|
||||||
|
|
||||||
builder.Services.AddYouTubeServices();
|
|
||||||
|
|
||||||
builder.Services.AddRepositories();
|
|
||||||
|
|
||||||
builder.Services.AddScoped<IFacebookServices, FacebookTokenService>();
|
|
||||||
|
|
||||||
builder.Services.Configure<MongoDbSettings>(
|
|
||||||
builder.Configuration.GetSection("MongoDbSettings"));
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
var locOptions = app.Services.GetService<IOptions<RequestLocalizationOptions>>();
|
var locOptions = app.Services.GetService<IOptions<RequestLocalizationOptions>>();
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
},
|
},
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"applicationUrl": "https://localhost:7078"
|
"applicationUrl": "https://localhost:7078;http://localhost:5094"
|
||||||
},
|
},
|
||||||
"IIS Express": {
|
"IIS Express": {
|
||||||
"commandName": "IISExpress",
|
"commandName": "IISExpress",
|
||||||
|
|||||||
@ -1,167 +0,0 @@
|
|||||||
@{
|
|
||||||
ViewData["Title"] = "Adicionar Canal";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<h2>Adicionar Canal</h2>
|
|
||||||
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="col-md-8">
|
|
||||||
@if (TempData["Error"] != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
@TempData["Error"]
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<ul class="nav nav-tabs card-header-tabs" id="channelAddTabs" role="tablist">
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link active" id="search-tab" data-toggle="tab" href="#search" role="tab" aria-controls="search" aria-selected="true">
|
|
||||||
Buscar Canal
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" id="id-tab" data-toggle="tab" href="#id" role="tab" aria-controls="id" aria-selected="false">
|
|
||||||
Inserir ID do Canal
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="tab-content" id="channelAddTabsContent">
|
|
||||||
<div class="tab-pane fade show active" id="search" role="tabpanel" aria-labelledby="search-tab">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="channelSearch">Buscar Canal</label>
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" id="channelSearch" placeholder="Digite o nome do canal..." minlength="3">
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button class="btn btn-primary" type="button" id="searchButton">
|
|
||||||
<i class="bi bi-search"></i> Buscar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<small class="form-text text-muted">Insira pelo menos 3 caracteres para buscar.</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="searchResults" class="mt-4"></div>
|
|
||||||
</div>
|
|
||||||
<div class="tab-pane fade" id="id" role="tabpanel" aria-labelledby="id-tab">
|
|
||||||
<form asp-action="Add" method="post">
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="channelId">ID do Canal</label>
|
|
||||||
<input type="text" class="form-control" id="channelId" name="channelId" required
|
|
||||||
placeholder="Ex: UCkw4JCwteGrDHIsyIIKo4tQ">
|
|
||||||
<small class="form-text text-muted">
|
|
||||||
O ID do canal pode ser encontrado na URL do canal, ex: youtube.com/channel/<strong>UCkw4JCwteGrDHIsyIIKo4tQ</strong>
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Adicionar Canal</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-3">
|
|
||||||
<a href="@Url.Action("Index", "Channels")" class="btn btn-link">
|
|
||||||
<i class="bi bi-arrow-left"></i> Voltar para Meus Canais
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card bg-light">
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">Dica</h5>
|
|
||||||
<p class="card-text">
|
|
||||||
Adicione os canais do YouTube que você gerencia ou tem permissão para acessar.
|
|
||||||
Depois de adicionar os canais, você poderá ver os vídeos deles na seção "Vídeos".
|
|
||||||
</p>
|
|
||||||
<h6 class="mt-3">Como encontrar o ID do canal?</h6>
|
|
||||||
<ol class="small pl-3">
|
|
||||||
<li>Visite o canal no YouTube</li>
|
|
||||||
<li>Observe a URL: <code>youtube.com/channel/ID-DO-CANAL</code></li>
|
|
||||||
<li>Copie o ID que aparece após "/channel/"</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(function() {
|
|
||||||
$('#searchButton').click(function() {
|
|
||||||
searchChannels();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#channelSearch').keypress(function(e) {
|
|
||||||
if (e.which === 13) {
|
|
||||||
e.preventDefault();
|
|
||||||
searchChannels();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function searchChannels() {
|
|
||||||
var query = $('#channelSearch').val().trim();
|
|
||||||
|
|
||||||
if (query.length < 3) {
|
|
||||||
alert('Por favor, insira pelo menos 3 caracteres para buscar.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#searchResults').html('<div class="text-center"><div class="spinner-border text-primary" role="status"></div><p class="mt-2">Buscando canais...</p></div>');
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '@Url.Action("Search", "Channels")',
|
|
||||||
type: 'GET',
|
|
||||||
data: { query: query },
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
displaySearchResults(response.data);
|
|
||||||
} else {
|
|
||||||
$('#searchResults').html('<div class="alert alert-danger">' + response.message + '</div>');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: function() {
|
|
||||||
$('#searchResults').html('<div class="alert alert-danger">Erro ao buscar canais. Tente novamente.</div>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function displaySearchResults(channels) {
|
|
||||||
if (!channels || channels.length === 0) {
|
|
||||||
$('#searchResults').html('<div class="alert alert-info">Nenhum canal encontrado com este termo.</div>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var html = '<h5>Resultados da busca</h5><div class="list-group">';
|
|
||||||
|
|
||||||
channels.forEach(function(channel) {
|
|
||||||
html += '<div class="list-group-item list-group-item-action">' +
|
|
||||||
'<div class="d-flex w-100">' +
|
|
||||||
'<div class="mr-3"><img src="' + channel.thumbnailUrl + '" alt="' + channel.title + '" width="50" class="rounded"></div>' +
|
|
||||||
'<div>' +
|
|
||||||
'<h6 class="mb-1">' + channel.title + '</h6>' +
|
|
||||||
'<p class="mb-1 small text-muted">' + (channel.description ? (channel.description.substring(0, 100) + '...') : '') + '</p>' +
|
|
||||||
'<form action="@Url.Action("Add", "Channels")" method="post">' +
|
|
||||||
'<input type="hidden" name="channelId" value="' + channel.id + '">' +
|
|
||||||
'<div class="d-flex justify-content-between align-items-center mt-2">' +
|
|
||||||
'<small class="text-muted">ID: ' + channel.id + '</small><br/>' +
|
|
||||||
'<button type="submit" class="btn btn-sm btn-primary">Adicionar</button>' +
|
|
||||||
'</div>' +
|
|
||||||
'</form>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>' +
|
|
||||||
'</div>';
|
|
||||||
});
|
|
||||||
|
|
||||||
html += '</div>';
|
|
||||||
$('#searchResults').html(html);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
@using Postall.Domain.Dtos
|
|
||||||
@model List<ChannelResponse>
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Meus Canais";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<h2>Meus Canais</h2>
|
|
||||||
<p class="text-muted">Gerencie seus canais do YouTube</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 text-right">
|
|
||||||
<a href="@Url.Action("Add", "Channels")" class="btn btn-primary">
|
|
||||||
<i class="bi bi-plus-circle"></i> Adicionar Canal
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (TempData["Message"] != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
||||||
@TempData["Message"]
|
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (TempData["Error"] != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
||||||
@TempData["Error"]
|
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
@if (Model != null && Model.Any())
|
|
||||||
{
|
|
||||||
foreach (var channel in Model)
|
|
||||||
{
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<img src="@channel.ThumbnailUrl" alt="@channel.Title" class="img-fluid rounded">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<h5 class="card-title">@channel.Title</h5>
|
|
||||||
<p class="card-text text-muted small">
|
|
||||||
@(channel.Description?.Length > 100 ? channel.Description.Substring(0, 100) + "..." : channel.Description)
|
|
||||||
</p>
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<small class="text-muted">
|
|
||||||
<i class="bi bi-people"></i> @channel.SubscriberCount.ToString("N0") inscritos
|
|
||||||
</small>
|
|
||||||
<small class="text-muted">
|
|
||||||
<i class="bi bi-collection-play"></i> @channel.VideoCount.ToString("N0") vídeos
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer bg-white d-flex justify-content-between">
|
|
||||||
<a href="@channel.ChannelUrl" target="_blank" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="bi bi-youtube"></i> Ver no YouTube
|
|
||||||
</a>
|
|
||||||
<form asp-action="Remove" asp-controller="Channels" method="post"
|
|
||||||
onsubmit="return confirm('Tem certeza que deseja remover este canal da sua lista?');">
|
|
||||||
<input type="hidden" name="id" value="@channel.Id" />
|
|
||||||
<button type="submit" class="btn btn-sm btn-outline-danger">
|
|
||||||
<i class="bi bi-trash"></i> Remover
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="bi bi-info-circle"></i> Você ainda não possui canais. Clique em "Adicionar Canal" para começar.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -1,18 +1,11 @@
|
|||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Sites";
|
ViewData["Title"] = "Site";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
<div class="container">
|
<h1 class="display-4">Login</h1>
|
||||||
<div class="row justify-content-center align-items-center min-vh-100">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-6">
|
<div class="col-md-4">
|
||||||
<div class="card shadow-lg">
|
|
||||||
<div class="card-body text-center p-5">
|
|
||||||
<h2 class="card-title mb-4">Bem-vindo</h2>
|
|
||||||
<p class="card-text text-muted mb-4">
|
|
||||||
Faça login com sua conta Microsoft para acessar o chat.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form asp-action="FacebookLogin" method="get">
|
<form asp-action="FacebookLogin" method="get">
|
||||||
<button type="submit" class="btn btn-primary btn-block">
|
<button type="submit" class="btn btn-primary btn-block">
|
||||||
<i class="fab fa-facebook"></i> Login com Facebook
|
<i class="fab fa-facebook"></i> Login com Facebook
|
||||||
@ -20,35 +13,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@section Styles {
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
border: none;
|
|
||||||
border-radius: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #2f2f2f;
|
|
||||||
border: none;
|
|
||||||
padding: 12px;
|
|
||||||
transition: all 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: #404040;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
}
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
|
||||||
}
|
|
||||||
|
|||||||
@ -12,30 +12,6 @@
|
|||||||
<link rel="stylesheet" href="~/Postall.styles.css" asp-append-version="true" />
|
<link rel="stylesheet" href="~/Postall.styles.css" asp-append-version="true" />
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||||
@await RenderSectionAsync("Styles", required: false)
|
@await RenderSectionAsync("Styles", required: false)
|
||||||
<style>
|
|
||||||
.loading {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: rgba(255, 255, 255, 0.8);
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading .spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 4px solid #f3f3f3;
|
|
||||||
border-top: 4px solid #3498db;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="hide-body">
|
<body class="hide-body">
|
||||||
<partial name="_Busy" />
|
<partial name="_Busy" />
|
||||||
@ -50,24 +26,12 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
|
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-white" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link text-white" asp-area="" asp-controller="Plans" asp-action="Index">Planos</a>
|
<a class="nav-link text-white" asp-area="" asp-controller="Plans" asp-action="Index">Planos</a>
|
||||||
</li>
|
</li>
|
||||||
@if (User!=null && User.Identity!=null && User.Identity.IsAuthenticated)
|
|
||||||
{
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-white" asp-area="" asp-controller="Channels" asp-action="Index">Canais</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-white" asp-area="" asp-controller="Videos" asp-action="Index">Videos</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-white" asp-area="" asp-controller="OtherLogins" asp-action="Index">Sites</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link text-white" asp-area="" asp-controller="SocialMedia" asp-action="Index">Postagens</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
@if (User!=null && User.Identity!=null && !User.Identity.IsAuthenticated)
|
@if (User!=null && User.Identity!=null && !User.Identity.IsAuthenticated)
|
||||||
{
|
{
|
||||||
@ -80,6 +44,11 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link text-white" asp-area="" asp-controller="OtherLogins" asp-action="Index">Sites</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
<partial name="_Language"/>
|
<partial name="_Language"/>
|
||||||
<li class="nav-item dropdown" style="margin-right: 10px">
|
<li class="nav-item dropdown" style="margin-right: 10px">
|
||||||
@ -129,26 +98,8 @@
|
|||||||
$(function () {
|
$(function () {
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('.loading').hide();
|
$('.loading').hide();
|
||||||
$('#wrapper').show();
|
|
||||||
//$('body').fadeIn(1000);
|
//$('body').fadeIn(1000);
|
||||||
$('body').slideDown('slow');
|
$('body').slideDown('slow');
|
||||||
|
|
||||||
// Intercepta cliques em links
|
|
||||||
$('a:not([target="_blank"]):not([href^="#"]):not([href^="javascript"])').click(function(e) {
|
|
||||||
if (this.hostname === window.location.hostname) {
|
|
||||||
e.preventDefault();
|
|
||||||
const href = $(this).attr('href');
|
|
||||||
|
|
||||||
// Adiciona fade out
|
|
||||||
$('#wrapper').addClass('fade-out');
|
|
||||||
$('.loading').show();
|
|
||||||
|
|
||||||
// Navega após a animação
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = href;
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('a[href="#search"]').on('click', function (event) {
|
$('a[href="#search"]').on('click', function (event) {
|
||||||
@ -177,7 +128,7 @@
|
|||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
$('#wrapper').fadeIn('slow');
|
$('#wrapper').fadeIn('slow');
|
||||||
$('#navbarNav a.nav-link').click(function () {
|
$('a.nav-link').click(function () {
|
||||||
if ($(this).hasClass('dropdown-toggle')) {
|
if ($(this).hasClass('dropdown-toggle')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -229,19 +180,6 @@
|
|||||||
$('body').slideUp('slow');
|
$('body').slideUp('slow');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove fade-out quando a página carrega
|
|
||||||
$(window).on('pageshow', function() {
|
|
||||||
$('#wrapper').removeClass('fade-out');
|
|
||||||
$('.loading').hide();
|
|
||||||
setActiveByLocation();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gerencia submissão de formulários
|
|
||||||
$(document).on('submit', 'form', function () {
|
|
||||||
$('#wrapper').addClass('fade-out');
|
|
||||||
$('.loading').show();
|
|
||||||
});
|
|
||||||
|
|
||||||
//$(".hide-body").fadeOut(2000);
|
//$(".hide-body").fadeOut(2000);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,254 +0,0 @@
|
|||||||
@using Postall.Models
|
|
||||||
|
|
||||||
<!-- Views/SocialMedia/Index.cshtml -->
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Gerenciador de Postagens";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container-fluid mt-4">
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
||||||
<h2>Gerenciador de Postagens</h2>
|
|
||||||
<a href="@Url.Action("Post", "SocialMedia")" class="btn btn-primary">
|
|
||||||
<i class="bi bi-plus-circle"></i> Nova Postagem
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover" id="postsTable">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th>Canal</th>
|
|
||||||
<th>Título</th>
|
|
||||||
<th>Próxima Data</th>
|
|
||||||
<th>Última Atualização</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<!-- Os dados serão preenchidos via JavaScript -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Template para detalhes expandidos -->
|
|
||||||
<template id="detailsTemplate">
|
|
||||||
<div class="expanded-details p-3">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<h5 class="mb-3">Status das Redes Sociais</h5>
|
|
||||||
<div class="social-media-grid">
|
|
||||||
<!-- Facebook -->
|
|
||||||
<div class="social-media-item">
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<i class="bi bi-facebook me-2"></i>
|
|
||||||
<span class="platform-name">Facebook</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-badge">
|
|
||||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
|
||||||
</div>
|
|
||||||
<div class="next-date small text-muted mt-1">
|
|
||||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Instagram -->
|
|
||||||
<div class="social-media-item">
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<i class="bi bi-instagram me-2"></i>
|
|
||||||
<span class="platform-name">Instagram</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-badge">
|
|
||||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
|
||||||
</div>
|
|
||||||
<div class="next-date small text-muted mt-1">
|
|
||||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Twitter/X -->
|
|
||||||
<div class="social-media-item">
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<i class="bi bi-twitter-x me-2"></i>
|
|
||||||
<span class="platform-name">Twitter/X</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-badge">
|
|
||||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
|
||||||
</div>
|
|
||||||
<div class="next-date small text-muted mt-1">
|
|
||||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- WhatsApp -->
|
|
||||||
<div class="social-media-item">
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<i class="bi bi-whatsapp me-2"></i>
|
|
||||||
<span class="platform-name">WhatsApp</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-badge">
|
|
||||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
|
||||||
</div>
|
|
||||||
<div class="next-date small text-muted mt-1">
|
|
||||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Telegram -->
|
|
||||||
<div class="social-media-item">
|
|
||||||
<div class="d-flex align-items-center mb-2">
|
|
||||||
<i class="bi bi-telegram me-2"></i>
|
|
||||||
<span class="platform-name">Telegram</span>
|
|
||||||
</div>
|
|
||||||
<div class="status-badge">
|
|
||||||
<span class="badge rounded-pill status-placeholder">Status</span>
|
|
||||||
</div>
|
|
||||||
<div class="next-date small text-muted mt-1">
|
|
||||||
Próxima data: <span class="date-placeholder">--/--/----</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
@section Styles {
|
|
||||||
<style>
|
|
||||||
.social-media-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-media-item {
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid #dee2e6;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge .badge {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.status-publicado { background-color: #198754; }
|
|
||||||
.badge.status-nao-selecionado { background-color: #6c757d; }
|
|
||||||
.badge.status-gerado { background-color: #0dcaf0; }
|
|
||||||
.badge.status-agendado { background-color: #ffc107; color: #000; }
|
|
||||||
.badge.status-automatizado { background-color: #0d6efd; }
|
|
||||||
|
|
||||||
tr.expanded {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expanded-details {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-top: 1px solid #dee2e6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@media (max-width: 768px) {
|
|
||||||
.social-media-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
}
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script>
|
|
||||||
$(document).ready(function() {
|
|
||||||
loadPosts();
|
|
||||||
});
|
|
||||||
|
|
||||||
function loadPosts() {
|
|
||||||
$.get('@Url.Action("GetPostDetails", "SocialMedia")', function(data) {
|
|
||||||
if (data.success) {
|
|
||||||
renderPosts(data.posts);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderPosts(posts) {
|
|
||||||
const tbody = $('#postsTable tbody');
|
|
||||||
tbody.empty();
|
|
||||||
|
|
||||||
posts.forEach(post => {
|
|
||||||
const row = $(`
|
|
||||||
<tr data-post-id="${post.id}">
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-sm btn-link expand-btn">
|
|
||||||
<i class="bi bi-chevron-right"></i>
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td>${post.channel}</td>
|
|
||||||
<td>${post.title}</td>
|
|
||||||
<td>${formatDate(post.nextScheduledDate)}</td>
|
|
||||||
<td>${formatDate(post.lastUpdate)}</td>
|
|
||||||
</tr>
|
|
||||||
`);
|
|
||||||
|
|
||||||
tbody.append(row);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(dateString) {
|
|
||||||
if (!dateString) return '--/--/----';
|
|
||||||
const date = new Date(dateString);
|
|
||||||
return date.toLocaleDateString('pt-BR');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStatusBadgeClass(status) {
|
|
||||||
const statusMap = {
|
|
||||||
'publicado': 'status-publicado',
|
|
||||||
'não selecionado': 'status-nao-selecionado',
|
|
||||||
'gerado': 'status-gerado',
|
|
||||||
'agendado': 'status-agendado',
|
|
||||||
'automatizado': 'status-automatizado'
|
|
||||||
};
|
|
||||||
return statusMap[status.toLowerCase()] || '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).on('click', '.expand-btn', function() {
|
|
||||||
const row = $(this).closest('tr');
|
|
||||||
const postId = row.data('post-id');
|
|
||||||
const icon = $(this).find('i');
|
|
||||||
|
|
||||||
if (row.next().hasClass('details-row')) {
|
|
||||||
// Fechar detalhes
|
|
||||||
row.next().remove();
|
|
||||||
row.removeClass('expanded');
|
|
||||||
icon.removeClass('bi-chevron-down').addClass('bi-chevron-right');
|
|
||||||
} else {
|
|
||||||
// Abrir detalhes
|
|
||||||
$.get(`@Url.Action("GetSocialMediaStatus", "SocialMedia")?postId=${postId}`, function(data) {
|
|
||||||
if (data.success) {
|
|
||||||
const template = document.getElementById('detailsTemplate');
|
|
||||||
const detailsContent = template.content.cloneNode(true);
|
|
||||||
|
|
||||||
// Preencher os status
|
|
||||||
data.socialMediaStatus.forEach(status => {
|
|
||||||
const platformElement = $(detailsContent).find(`[data-platform="${status.platform}"]`);
|
|
||||||
platformElement.find('.status-placeholder')
|
|
||||||
.text(status.status)
|
|
||||||
.addClass(getStatusBadgeClass(status.status));
|
|
||||||
platformElement.find('.date-placeholder').text(formatDate(status.nextScheduledDate));
|
|
||||||
});
|
|
||||||
|
|
||||||
const detailsRow = $('<tr class="details-row">').append(
|
|
||||||
$('<td colspan="5">').append(detailsContent)
|
|
||||||
);
|
|
||||||
|
|
||||||
row.addClass('expanded');
|
|
||||||
row.after(detailsRow);
|
|
||||||
icon.removeClass('bi-chevron-right').addClass('bi-chevron-down');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
@ -1,347 +0,0 @@
|
|||||||
@using Postall.Models
|
|
||||||
@model PostViewModel
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Gerenciar Postagens";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container-fluid">
|
|
||||||
<div class="row">
|
|
||||||
<!-- Menu lateral em desktop / Menu superior em mobile -->
|
|
||||||
<div class="col-md-3 col-12 mb-3">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Plataformas</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex flex-wrap gap-2">
|
|
||||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="facebook">
|
|
||||||
Facebook
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="instagram">
|
|
||||||
Instagram
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="twitter">
|
|
||||||
Twitter/X
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="whatsapp">
|
|
||||||
WhatsApp
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-primary platform-btn" data-platform="telegram">
|
|
||||||
Telegram
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card mt-3">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Agendamento</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-check mb-2">
|
|
||||||
<input class="form-check-input" type="radio" name="schedule" id="manual" checked>
|
|
||||||
<label class="form-check-label" for="manual">Manual</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check mb-2">
|
|
||||||
<input class="form-check-input" type="radio" name="schedule" id="weekly">
|
|
||||||
<label class="form-check-label" for="weekly">Semanal</label>
|
|
||||||
</div>
|
|
||||||
<div id="weeklyOptions" class="d-none">
|
|
||||||
<div class="mb-2">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input weekday-check" type="checkbox" value="1" id="monday">
|
|
||||||
<label class="form-check-label" for="monday">Segunda-feira</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input weekday-check" type="checkbox" value="2" id="tuesday">
|
|
||||||
<label class="form-check-label" for="tuesday">Terça-feira</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input weekday-check" type="checkbox" value="3" id="wednesday">
|
|
||||||
<label class="form-check-label" for="wednesday">Quarta-feira</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input weekday-check" type="checkbox" value="4" id="thursday">
|
|
||||||
<label class="form-check-label" for="thursday">Quinta-feira</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input weekday-check" type="checkbox" value="5" id="friday">
|
|
||||||
<label class="form-check-label" for="friday">Sexta-feira</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input weekday-check" type="checkbox" value="6" id="saturday">
|
|
||||||
<label class="form-check-label" for="saturday">Sábado</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input weekday-check" type="checkbox" value="0" id="sunday">
|
|
||||||
<label class="form-check-label" for="sunday">Domingo</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input type="time" class="form-control" id="scheduleTime">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preview lateral -->
|
|
||||||
<div class="col-md-6 col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Video</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">Canal</label>
|
|
||||||
<input type="text" class="form-control" id="previewChannel" readonly>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">URL do Vídeo</label>
|
|
||||||
<input type="text" class="form-control" id="previewVideoUrl">
|
|
||||||
</div>
|
|
||||||
<div class="preview-content">
|
|
||||||
<h6>Título</h6>
|
|
||||||
<p id="previewTitle"></p>
|
|
||||||
<h6>Conteúdo</h6>
|
|
||||||
<p id="previewContent"></p>
|
|
||||||
<div id="previewImage" class="mt-2"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button type="button" class="btn btn-secondary me-2" onclick="savePlatformDraft()">Gerar com IA</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Nova Postagem</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="postTitle" class="form-label">Título</label>
|
|
||||||
<input type="text" class="form-control" id="postTitle">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="postContent" class="form-label">Conteúdo</label>
|
|
||||||
<textarea class="form-control" id="postContent" rows="5"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="postImage" class="form-label">Imagem</label>
|
|
||||||
<input type="file" class="form-control" id="postImage" accept="image/*">
|
|
||||||
</div>
|
|
||||||
<div class="preview-image mb-3 d-none">
|
|
||||||
<img id="imagePreview" src="#" alt="Preview" class="img-fluid">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<button type="button" class="btn btn-secondary me-2" onclick="saveDraft()">Salvar Rascunho</button>
|
|
||||||
<button type="button" class="btn btn-primary" onclick="publishPost()">Publicar</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script>
|
|
||||||
let selectedPlatform = null;
|
|
||||||
|
|
||||||
// Gestão das plataformas
|
|
||||||
document.querySelectorAll('.platform-btn').forEach(btn => {
|
|
||||||
btn.addEventListener('click', async function() {
|
|
||||||
const platform = this.dataset.platform;
|
|
||||||
|
|
||||||
// Reset outros botões
|
|
||||||
document.querySelectorAll('.platform-btn').forEach(b =>
|
|
||||||
b.classList.replace('btn-primary', 'btn-outline-primary'));
|
|
||||||
|
|
||||||
// Ativar botão selecionado
|
|
||||||
this.classList.replace('btn-outline-primary', 'btn-primary');
|
|
||||||
selectedPlatform = platform;
|
|
||||||
|
|
||||||
// Carregar dados da plataforma
|
|
||||||
await loadPlatformData(platform);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadPlatformData(platform) {
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/socialMedia/platformData/${platform}`);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
// Atualizar preview
|
|
||||||
document.getElementById('previewChannel').value = data.channel;
|
|
||||||
document.getElementById('previewVideoUrl').value = data.videoUrl;
|
|
||||||
document.getElementById('previewTitle').textContent = data.title;
|
|
||||||
document.getElementById('previewContent').textContent = data.content;
|
|
||||||
|
|
||||||
if (data.imageUrl) {
|
|
||||||
document.getElementById('previewImage').innerHTML =
|
|
||||||
`<img src="${data.imageUrl}" class="img-fluid" alt="Preview">`;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro ao carregar dados da plataforma:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mostrar/ocultar opções de agendamento semanal
|
|
||||||
document.getElementById('weekly').addEventListener('change', function() {
|
|
||||||
document.getElementById('weeklyOptions').classList.remove('d-none');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('manual').addEventListener('change', function() {
|
|
||||||
document.getElementById('weeklyOptions').classList.add('d-none');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Preview da imagem
|
|
||||||
document.getElementById('postImage').addEventListener('change', function(e) {
|
|
||||||
if (e.target.files && e.target.files[0]) {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = function(e) {
|
|
||||||
document.getElementById('imagePreview').src = e.target.result;
|
|
||||||
document.querySelector('.preview-image').classList.remove('d-none');
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(e.target.files[0]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function getSelectedPlatform() {
|
|
||||||
return selectedPlatform;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSelectedWeekDays() {
|
|
||||||
return Array.from(document.querySelectorAll('.weekday-check:checked'))
|
|
||||||
.map(checkbox => parseInt(checkbox.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function generateAIContent() {
|
|
||||||
if (!selectedPlatform) {
|
|
||||||
alert('Selecione uma plataforma primeiro!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/socialMedia/generateContent', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
platform: selectedPlatform,
|
|
||||||
videoUrl: document.getElementById('previewVideoUrl').value
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.success) {
|
|
||||||
document.getElementById('previewTitle').textContent = data.title;
|
|
||||||
document.getElementById('previewContent').textContent = data.content;
|
|
||||||
} else {
|
|
||||||
alert('Erro ao gerar conteúdo: ' + data.message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro ao gerar conteúdo:', error);
|
|
||||||
alert('Erro ao gerar conteúdo');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function savePlatformDraft() {
|
|
||||||
if (!selectedPlatform) {
|
|
||||||
alert('Selecione uma plataforma primeiro!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
platform: selectedPlatform,
|
|
||||||
title: document.getElementById('previewTitle').textContent,
|
|
||||||
content: document.getElementById('previewContent').textContent,
|
|
||||||
videoUrl: document.getElementById('previewVideoUrl').value
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/socialMedia/savePlatformDraft', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
if (result.success) {
|
|
||||||
alert('Rascunho salvo com sucesso!');
|
|
||||||
} else {
|
|
||||||
alert('Erro ao salvar rascunho: ' + result.message);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Erro ao salvar rascunho:', error);
|
|
||||||
alert('Erro ao salvar rascunho');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function publishPost() {
|
|
||||||
const isWeekly = document.getElementById('weekly').checked;
|
|
||||||
const data = {
|
|
||||||
title: document.getElementById('postTitle').value,
|
|
||||||
content: document.getElementById('postContent').value,
|
|
||||||
platform: getSelectedPlatform()
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isWeekly) {
|
|
||||||
data.isManual = false;
|
|
||||||
data.weekDays = getSelectedWeekDays();
|
|
||||||
data.time = document.getElementById('scheduleTime').value;
|
|
||||||
|
|
||||||
$.ajax({
|
|
||||||
url: '@Url.Action("SchedulePost", "SocialMedia")',
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
alert('Post agendado com sucesso!');
|
|
||||||
} else {
|
|
||||||
alert('Erro ao agendar post: ' + response.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$.ajax({
|
|
||||||
url: '@Url.Action("PublishPost", "SocialMedia")',
|
|
||||||
type: 'POST',
|
|
||||||
contentType: 'application/json',
|
|
||||||
data: JSON.stringify(data),
|
|
||||||
success: function(response) {
|
|
||||||
if (response.success) {
|
|
||||||
alert('Post publicado com sucesso!');
|
|
||||||
} else {
|
|
||||||
alert('Erro ao publicar post: ' + response.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.platform-btn {
|
|
||||||
min-width: 120px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-image img {
|
|
||||||
max-height: 300px;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@media (max-width: 768px) {
|
|
||||||
.container-fluid {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,169 +0,0 @@
|
|||||||
@model List<Postall.Models.VideoViewModel>
|
|
||||||
@{
|
|
||||||
ViewData["Title"] = "Gerenciador de Vídeos";
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="container mt-4">
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-md-8">
|
|
||||||
<h2>Meus Vídeos</h2>
|
|
||||||
<p class="text-muted">Gerencie seus vídeos do YouTube</p>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4 text-right">
|
|
||||||
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#addVideosModal">
|
|
||||||
<i class="bi bi-plus-circle"></i> Adicionar Vídeos
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (TempData["Message"] != null)
|
|
||||||
{
|
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
||||||
@TempData["Message"]
|
|
||||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
@if (Model != null && Model.Any())
|
|
||||||
{
|
|
||||||
foreach (var video in Model)
|
|
||||||
{
|
|
||||||
<div class="col-md-6 mb-4">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header bg-transparent">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="mb-0">@video.Title</h5>
|
|
||||||
<button class="btn btn-sm btn-link" type="button" data-toggle="collapse"
|
|
||||||
data-target="#collapse-@video.Id" aria-expanded="false">
|
|
||||||
<i class="bi bi-chevron-down"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-5">
|
|
||||||
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-7">
|
|
||||||
<p class="card-text">@(video.Description?.Length > 100 ? video.Description.Substring(0, 100) + "..." : video.Description)</p>
|
|
||||||
<p class="text-muted small">Publicado em: @video.PublishedAt.ToString("dd/MM/yyyy")</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="collapse" id="collapse-@video.Id">
|
|
||||||
<div class="card-footer bg-white">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<a href="@video.VideoUrl" target="_blank" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="bi bi-youtube"></i> Ver no YouTube
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary mr-1">
|
|
||||||
<i class="bi bi-pencil"></i> Editar
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-danger">
|
|
||||||
<i class="bi bi-trash"></i> Remover
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<i class="bi bi-info-circle"></i> Você ainda não possui vídeos. Clique em "Adicionar Vídeos" para começar.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal para adicionar vídeos -->
|
|
||||||
<div class="modal fade" id="addVideosModal" tabindex="-1" role="dialog" aria-labelledby="addVideosModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-lg" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="addVideosModalLabel">Adicionar Vídeos do YouTube</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div id="channelVideosContainer">
|
|
||||||
<div class="text-center">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="sr-only">Carregando...</span>
|
|
||||||
</div>
|
|
||||||
<p class="mt-2">Carregando vídeos...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="btnAddSelectedVideos">Adicionar Selecionados</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@section Scripts {
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(function () {
|
|
||||||
// Carrega os vídeos do canal quando o modal é aberto
|
|
||||||
$('#addVideosModal').on('shown.bs.modal', function () {
|
|
||||||
loadChannelVideos();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Função para carregar os vídeos do canal
|
|
||||||
function loadChannelVideos() {
|
|
||||||
$.ajax({
|
|
||||||
url: '@Url.Action("GetChannelVideos", "Videos")',
|
|
||||||
type: 'GET',
|
|
||||||
success: function (result) {
|
|
||||||
$('#channelVideosContainer').html(result);
|
|
||||||
},
|
|
||||||
error: function (error) {
|
|
||||||
$('#channelVideosContainer').html('<div class="alert alert-danger">Erro ao carregar vídeos. Tente novamente.</div>');
|
|
||||||
console.error('Erro:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manipula o clique no botão de adicionar vídeos selecionados
|
|
||||||
$('#btnAddSelectedVideos').click(function () {
|
|
||||||
var selectedVideos = [];
|
|
||||||
|
|
||||||
// Coleta todos os checkboxes selecionados
|
|
||||||
$('.video-checkbox:checked').each(function () {
|
|
||||||
selectedVideos.push($(this).val());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (selectedVideos.length === 0) {
|
|
||||||
alert('Selecione pelo menos um vídeo para adicionar.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cria um formulário para enviar os IDs dos vídeos selecionados
|
|
||||||
var form = $('<form action="@Url.Action("AddVideos", "Videos")" method="post"></form>');
|
|
||||||
|
|
||||||
// Adiciona inputs ocultos para cada vídeo selecionado
|
|
||||||
selectedVideos.forEach(function (videoId) {
|
|
||||||
form.append('<input type="hidden" name="selectedVideos" value="' + videoId + '">');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Adiciona o formulário ao corpo do documento e o submete
|
|
||||||
$('body').append(form);
|
|
||||||
form.submit();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
@model List<Postall.Models.VideoViewModel>
|
|
||||||
|
|
||||||
<form id="channelVideosForm">
|
|
||||||
<h6 class="mb-3">Últimos 10 vídeos disponíveis (ordenados por data):</h6>
|
|
||||||
|
|
||||||
@if (Model != null && Model.Any())
|
|
||||||
{
|
|
||||||
<div class="list-group">
|
|
||||||
@foreach (var video in Model)
|
|
||||||
{
|
|
||||||
<div class="list-group-item list-group-item-action flex-column align-items-start">
|
|
||||||
<div class="d-flex">
|
|
||||||
<div class="checkbox-container mr-3 mt-1">
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input video-checkbox" type="checkbox" value="@video.Id" id="video-@video.Id">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row w-100">
|
|
||||||
<div class="col-md-4">
|
|
||||||
<img src="@video.ThumbnailUrl" alt="@video.Title" class="img-fluid rounded">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-8">
|
|
||||||
<div class="d-flex w-100 justify-content-between">
|
|
||||||
<h5 class="mb-1">@video.Title</h5>
|
|
||||||
<small>@video.PublishedAt.ToString("dd/MM/yyyy")</small>
|
|
||||||
</div>
|
|
||||||
<p class="mb-1">@video.Description</p>
|
|
||||||
<small class="text-muted">
|
|
||||||
<i class="bi bi-youtube"></i>
|
|
||||||
@(video.ChannelTitle ?? "Seu Canal")
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<div class="alert alert-info">
|
|
||||||
Nenhum vídeo encontrado nos canais aos quais você tem acesso.
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.checkbox-container {
|
|
||||||
min-width: 30px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -4,10 +4,5 @@
|
|||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"MongoDbSettings": {
|
|
||||||
"ConnectionString": "mongodb://localhost:27017",
|
|
||||||
"DatabaseName": "YouTubeChannelsDB",
|
|
||||||
"ChannelsCollectionName": "Channels"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,16 +18,8 @@
|
|||||||
"AppSecret": "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"
|
"AppSecret": "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"
|
||||||
},
|
},
|
||||||
"Facebook": {
|
"Facebook": {
|
||||||
"AppId": "963281005306692",
|
"AppId": "seu_app_id_aqui",
|
||||||
"AppSecret": "575839ccbb36d1457715f1f6dd0a8db9"
|
"AppSecret": "seu_app_secret_aqui"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Youtube": {
|
|
||||||
"ApiKey": "AIzaSyBwFTW5WRAaBupyTgayVIeaS3LjGn0gpNI"
|
|
||||||
},
|
|
||||||
"MongoDbSettings": {
|
|
||||||
"ConnectionString": "mongodb://localhost:27017",
|
|
||||||
"DatabaseName": "YouTubeChannelsDB",
|
|
||||||
"ChannelsCollectionName": "Channels"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,82 +0,0 @@
|
|||||||
/* Estilos para a página de vídeos */
|
|
||||||
|
|
||||||
.card {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 1px solid rgba(0,0,0,.125);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header button.btn-link {
|
|
||||||
text-decoration: none;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header button.btn-link:hover {
|
|
||||||
color: #007bff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body img {
|
|
||||||
max-height: 120px;
|
|
||||||
object-fit: cover;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-checkbox {
|
|
||||||
cursor: pointer;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item {
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item:hover {
|
|
||||||
background-color: rgba(0,123,255,.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-group-item img {
|
|
||||||
max-height: 100px;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animação para o colapso */
|
|
||||||
.collapse {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilo para o botão de expansão do card */
|
|
||||||
.card-header button.btn-link {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header button.btn-link[aria-expanded="true"] i {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ajustes para responsividade */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.col-md-6.mb-4 {
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body .row {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body .col-md-5,
|
|
||||||
.card-body .col-md-7 {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
flex: 0 0 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body .col-md-5 {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user