Creazione progetto InstaArchive WinUI 3 (.NET 8)

Aggiunta di tutti i file sorgente, configurazione e risorse per la nuova app desktop InstaArchive. Implementati servizi per monitoraggio e archiviazione automatica di contenuti Instagram (post, storie, reels, highlights) con persistenza locale, gestione utenti, impostazioni avanzate, dashboard e interfaccia moderna in italiano. Integrazione MVVM, rate limiting, iniezione metadati e funzionalità di import/export.
This commit is contained in:
2026-01-07 14:03:34 +01:00
parent 738b70ac9b
commit d2ca019d64
38 changed files with 2789 additions and 0 deletions

36
InstaArchive.sln Normal file
View File

@@ -0,0 +1,36 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.1.11312.151 d18.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstaArchive", "Teti\InstaArchive.csproj", "{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Debug|ARM64.ActiveCfg = Debug|ARM64
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Debug|ARM64.Build.0 = Debug|ARM64
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Debug|x64.ActiveCfg = Debug|x64
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Debug|x64.Build.0 = Debug|x64
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Debug|x86.ActiveCfg = Debug|x86
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Debug|x86.Build.0 = Debug|x86
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Release|ARM64.ActiveCfg = Release|ARM64
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Release|ARM64.Build.0 = Release|ARM64
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Release|x64.ActiveCfg = Release|x64
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Release|x64.Build.0 = Release|x64
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Release|x86.ActiveCfg = Release|x86
{A8B5E5F6-3C4D-4E8F-9A7B-1C2D3E4F5A6B}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {288F5AF5-3DD0-4A6A-9FE4-D3663967FE8C}
EndGlobalSection
EndGlobal

18
Teti/App.xaml Normal file
View File

@@ -0,0 +1,18 @@
<Application
x:Class="InstaArchive.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:InstaArchive.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<converters:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
<converters:TimeFormatConverter x:Key="TimeFormatConverter"/>
</ResourceDictionary>
</Application.Resources>
</Application>

71
Teti/App.xaml.cs Normal file
View File

@@ -0,0 +1,71 @@
using Microsoft.UI.Xaml;
using Microsoft.Extensions.DependencyInjection;
using System;
using InstaArchive.Services;
using InstaArchive.Repositories;
using InstaArchive.ViewModels;
namespace InstaArchive;
public partial class App : Application
{
public static IServiceProvider Services { get; private set; } = null!;
public static Window MainWindow { get; private set; } = null!;
public App()
{
// Initialize Windows App SDK for unpackaged app
InitializeWindowsAppSDK();
InitializeComponent();
ConfigureServices();
}
private void InitializeWindowsAppSDK()
{
// For unpackaged apps, we need to initialize the Windows App SDK
// This will be called before InitializeComponent
try
{
// The bootstrap initialization happens automatically when WindowsAppSDKSelfContained is set to true
// If you need manual initialization, uncomment the following:
// Microsoft.Windows.ApplicationModel.DynamicDependency.Bootstrap.Initialize(0x00010005); // Version 1.5
}
catch (Exception ex)
{
// Log or handle initialization failure
System.Diagnostics.Debug.WriteLine($"Failed to initialize Windows App SDK: {ex.Message}");
}
}
private void ConfigureServices()
{
var services = new ServiceCollection();
// Repositories
services.AddSingleton<FileBasedUserRepository>();
services.AddSingleton<FileBasedMediaRepository>();
// Services
services.AddSingleton<SettingsService>();
services.AddSingleton<UserManagementService>();
services.AddSingleton<FileSystemService>();
services.AddSingleton<InstagramSessionService>();
services.AddSingleton<MediaDownloaderService>();
services.AddSingleton<MetadataInjectionService>();
services.AddSingleton<SchedulerService>();
// ViewModels
services.AddTransient<DashboardViewModel>();
services.AddTransient<TargetsViewModel>();
services.AddTransient<SettingsViewModel>();
Services = services.BuildServiceProvider();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
MainWindow = new MainWindow();
MainWindow.Activate();
}
}

View File

View File

View File

View File

@@ -0,0 +1,61 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
using System;
namespace InstaArchive.Converters;
public class BoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value is bool b && b ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return value is Visibility v && v == Visibility.Visible;
}
}
public class InverseBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value is bool b && !b ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
return value is Visibility v && v == Visibility.Collapsed;
}
}
public class NullToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
return value != null ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
public class TimeFormatConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is DateTime dt)
{
return dt.ToString("HH:mm:ss");
}
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}

48
Teti/InstaArchive.csproj Normal file
View File

@@ -0,0 +1,48 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>InstaArchive</RootNamespace>
<AssemblyName>InstaArchive</AssemblyName>
<ApplicationTitle>InstaArchive</ApplicationTitle>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<EnableMsixTooling>true</EnableMsixTooling>
<Nullable>enable</Nullable>
<LangVersion>12</LangVersion>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.5.240311000" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.3233" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2420.47" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<Folder Include="Data\" />
</ItemGroup>
</Project>

24
Teti/MainWindow.xaml Normal file
View File

@@ -0,0 +1,24 @@
<Window
x:Class="InstaArchive.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="InstaArchive">
<Grid>
<NavigationView x:Name="NavView"
IsBackButtonVisible="Collapsed"
PaneDisplayMode="Left"
SelectionChanged="NavView_SelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItem Content="Pannello" Tag="Dashboard" Icon="Home"/>
<NavigationViewItem Content="Obiettivi" Tag="Targets" Icon="People"/>
<NavigationViewItem Content="Impostazioni" Tag="Settings" Icon="Setting"/>
</NavigationView.MenuItems>
<Frame x:Name="ContentFrame"/>
</NavigationView>
</Grid>
</Window>

38
Teti/MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,38 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using InstaArchive.Views;
namespace InstaArchive;
public sealed partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ExtendsContentIntoTitleBar = true;
SetTitleBar(null);
ContentFrame.Navigate(typeof(DashboardPage));
}
private void NavView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
if (args.SelectedItem is NavigationViewItem item)
{
var tag = item.Tag?.ToString();
Type? pageType = tag switch
{
"Dashboard" => typeof(DashboardPage),
"Targets" => typeof(TargetsPage),
"Settings" => typeof(SettingsPage),
_ => null
};
if (pageType != null)
{
ContentFrame.Navigate(pageType);
}
}
}
}

View File

@@ -0,0 +1,28 @@
namespace InstaArchive.Models;
public class AppSettings
{
public string BasePath { get; set; } = @"C:\InstaArchive\Data";
public bool EnableDateSubfolders { get; set; } = false;
public bool EnableMetadataInjection { get; set; } = true;
public int GlobalStoryCheckInterval { get; set; } = 10;
public int GlobalPostCheckInterval { get; set; } = 1440;
public int MaxConcurrentDownloads { get; set; } = 3;
public bool EnableRateLimiting { get; set; } = true;
public int RateLimitRequestsPerHour { get; set; } = 200;
public int BackoffBaseDelaySeconds { get; set; } = 30;
public int BackoffMaxAttempts { get; set; } = 5;
public string DateFolderFormat { get; set; } = "yyyy-MM-dd";
public bool AutoStartMonitoring { get; set; } = false;
}

View File

@@ -0,0 +1,38 @@
using System;
namespace InstaArchive.Models;
public class InstagramUser
{
public long UserId { get; set; }
public string CurrentUsername { get; set; } = string.Empty;
public string? Biography { get; set; }
public string? ProfilePictureUrl { get; set; }
public DateTime AddedDate { get; set; }
public DateTime LastUpdated { get; set; }
// Monitoring Configuration
public bool MonitorPosts { get; set; } = true;
public bool MonitorStories { get; set; } = true;
public bool MonitorReels { get; set; } = true;
public bool MonitorHighlights { get; set; } = false;
// Scheduling Configuration (in minutes)
public int StoriesCheckInterval { get; set; } = 10;
public int PostsCheckInterval { get; set; } = 1440; // 24 hours
// Path Override
public string? CustomBasePath { get; set; }
// Username History (JSON serialized)
public string UsernameHistoryJson { get; set; } = "[]";
}

19
Teti/Models/LogEntry.cs Normal file
View File

@@ -0,0 +1,19 @@
using System;
namespace InstaArchive.Models;
public enum LogLevel
{
Info,
Warning,
Error,
Success
}
public class LogEntry
{
public DateTime Timestamp { get; set; }
public LogLevel Level { get; set; }
public string Message { get; set; } = string.Empty;
public string? Details { get; set; }
}

41
Teti/Models/MediaItem.cs Normal file
View File

@@ -0,0 +1,41 @@
using System;
namespace InstaArchive.Models;
public enum MediaType
{
Photo,
Video,
Story,
Reel,
Highlight
}
public class MediaItem
{
public long InstagramMediaId { get; set; }
public long UserId { get; set; }
public MediaType MediaType { get; set; }
public string FileName { get; set; } = string.Empty;
public string LocalPath { get; set; } = string.Empty;
public DateTime DownloadedAt { get; set; }
public DateTime? PostedAt { get; set; }
public string? Caption { get; set; }
public string? Location { get; set; }
public double? Latitude { get; set; }
public double? Longitude { get; set; }
public long FileSize { get; set; }
public string? Url { get; set; }
}

View File

@@ -0,0 +1,9 @@
using System;
namespace InstaArchive.Models;
public class UsernameHistoryEntry
{
public string Username { get; set; } = string.Empty;
public DateTime ChangedAt { get; set; }
}

View File

@@ -0,0 +1,201 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using InstaArchive.Models;
using InstaArchive.Services;
namespace InstaArchive.Repositories;
public class FileBasedMediaRepository
{
private readonly SettingsService _settingsService;
public FileBasedMediaRepository(SettingsService settingsService)
{
_settingsService = settingsService;
}
public async Task<List<MediaItem>> GetAllMediaAsync()
{
var allMedia = new List<MediaItem>();
var settings = _settingsService.GetSettings();
if (!Directory.Exists(settings.BasePath))
{
return allMedia;
}
var userDirectories = Directory.GetDirectories(settings.BasePath);
foreach (var userDir in userDirectories)
{
var userIdStr = Path.GetFileName(userDir);
if (!long.TryParse(userIdStr, out var userId))
{
continue;
}
var userMedia = await GetMediaByUserIdAsync(userId);
allMedia.AddRange(userMedia);
}
return allMedia;
}
public async Task<List<MediaItem>> GetMediaByUserIdAsync(long userId)
{
var settings = _settingsService.GetSettings();
var userPath = Path.Combine(settings.BasePath, userId.ToString());
if (!Directory.Exists(userPath))
{
return new List<MediaItem>();
}
var mediaIndexPath = Path.Combine(userPath, "media_index.json");
if (!File.Exists(mediaIndexPath))
{
// Scansiona il file system per creare l'indice
return await ScanAndIndexMediaAsync(userId, userPath);
}
var json = await File.ReadAllTextAsync(mediaIndexPath);
return JsonConvert.DeserializeObject<List<MediaItem>>(json) ?? new List<MediaItem>();
}
public async Task<MediaItem?> GetMediaByInstagramIdAsync(long instagramMediaId)
{
var allMedia = await GetAllMediaAsync();
return allMedia.FirstOrDefault(m => m.InstagramMediaId == instagramMediaId);
}
public async Task AddMediaAsync(MediaItem media)
{
var settings = _settingsService.GetSettings();
var userPath = Path.Combine(settings.BasePath, media.UserId.ToString());
Directory.CreateDirectory(userPath);
var mediaList = await GetMediaByUserIdAsync(media.UserId);
mediaList.Add(media);
await SaveMediaIndexAsync(media.UserId, mediaList);
}
public async Task<bool> MediaExistsAsync(long instagramMediaId)
{
var media = await GetMediaByInstagramIdAsync(instagramMediaId);
return media != null;
}
public async Task<int> GetTotalMediaCountAsync()
{
var allMedia = await GetAllMediaAsync();
return allMedia.Count;
}
public async Task<int> GetTodayDownloadsCountAsync()
{
var allMedia = await GetAllMediaAsync();
var today = DateTime.UtcNow.Date;
return allMedia.Count(m => m.DownloadedAt.Date == today);
}
public async Task<long> GetTotalStorageUsedAsync()
{
var allMedia = await GetAllMediaAsync();
return allMedia.Sum(m => m.FileSize);
}
public async Task<List<MediaItem>> GetRecentMediaAsync(int count)
{
var allMedia = await GetAllMediaAsync();
return allMedia
.OrderByDescending(m => m.DownloadedAt)
.Take(count)
.ToList();
}
private async Task<List<MediaItem>> ScanAndIndexMediaAsync(long userId, string userPath)
{
var mediaItems = new List<MediaItem>();
var mediaTypes = new[] { "Feed", "Stories", "Reels", "Highlights" };
foreach (var mediaType in mediaTypes)
{
var typePath = Path.Combine(userPath, mediaType);
if (!Directory.Exists(typePath))
{
continue;
}
await ScanDirectoryRecursiveAsync(userId, typePath, mediaType, mediaItems);
}
await SaveMediaIndexAsync(userId, mediaItems);
return mediaItems;
}
private async Task ScanDirectoryRecursiveAsync(long userId, string directory, string mediaTypeName, List<MediaItem> mediaItems)
{
var files = Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories);
var validExtensions = new[] { ".jpg", ".jpeg", ".png", ".mp4", ".mov" };
foreach (var filePath in files)
{
var extension = Path.GetExtension(filePath).ToLower();
if (!validExtensions.Contains(extension))
{
continue;
}
var fileName = Path.GetFileName(filePath);
var fileInfo = new FileInfo(filePath);
// Prova a estrarre l'ID Instagram dal nome file
long instagramMediaId = 0;
var parts = Path.GetFileNameWithoutExtension(fileName).Split('_');
if (parts.Length >= 2)
{
long.TryParse(parts[1], out instagramMediaId);
}
var mediaType = mediaTypeName switch
{
"Stories" => MediaType.Story,
"Reels" => MediaType.Reel,
"Highlights" => MediaType.Highlight,
_ => extension == ".mp4" || extension == ".mov" ? MediaType.Video : MediaType.Photo
};
var mediaItem = new MediaItem
{
InstagramMediaId = instagramMediaId,
UserId = userId,
MediaType = mediaType,
FileName = fileName,
LocalPath = filePath,
DownloadedAt = fileInfo.CreationTimeUtc,
FileSize = fileInfo.Length
};
mediaItems.Add(mediaItem);
}
await Task.CompletedTask;
}
private async Task SaveMediaIndexAsync(long userId, List<MediaItem> mediaList)
{
var settings = _settingsService.GetSettings();
var userPath = Path.Combine(settings.BasePath, userId.ToString());
Directory.CreateDirectory(userPath);
var mediaIndexPath = Path.Combine(userPath, "media_index.json");
var json = JsonConvert.SerializeObject(mediaList, Formatting.Indented);
await File.WriteAllTextAsync(mediaIndexPath, json);
}
}

View File

@@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using InstaArchive.Models;
using InstaArchive.Services;
namespace InstaArchive.Repositories;
public class FileBasedUserRepository
{
private readonly SettingsService _settingsService;
private readonly string _usersIndexPath;
public FileBasedUserRepository(SettingsService settingsService)
{
_settingsService = settingsService;
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"InstaArchive"
);
Directory.CreateDirectory(appDataPath);
_usersIndexPath = Path.Combine(appDataPath, "users_index.json");
}
public async Task<List<InstagramUser>> GetAllUsersAsync()
{
if (!File.Exists(_usersIndexPath))
{
return new List<InstagramUser>();
}
var json = await File.ReadAllTextAsync(_usersIndexPath);
return JsonConvert.DeserializeObject<List<InstagramUser>>(json) ?? new List<InstagramUser>();
}
public async Task<InstagramUser?> GetUserByIdAsync(long userId)
{
var users = await GetAllUsersAsync();
return users.FirstOrDefault(u => u.UserId == userId);
}
public async Task<InstagramUser> AddUserAsync(InstagramUser user)
{
var users = await GetAllUsersAsync();
if (users.Any(u => u.UserId == user.UserId))
{
throw new InvalidOperationException($"User with ID {user.UserId} already exists");
}
user.AddedDate = DateTime.UtcNow;
user.LastUpdated = DateTime.UtcNow;
users.Add(user);
await SaveUsersAsync(users);
return user;
}
public async Task UpdateUserAsync(InstagramUser user)
{
var users = await GetAllUsersAsync();
var index = users.FindIndex(u => u.UserId == user.UserId);
if (index == -1)
{
throw new InvalidOperationException($"User with ID {user.UserId} not found");
}
user.LastUpdated = DateTime.UtcNow;
users[index] = user;
await SaveUsersAsync(users);
}
public async Task DeleteUserAsync(long userId)
{
var users = await GetAllUsersAsync();
users.RemoveAll(u => u.UserId == userId);
await SaveUsersAsync(users);
}
private async Task SaveUsersAsync(List<InstagramUser> users)
{
var json = JsonConvert.SerializeObject(users, Formatting.Indented);
await File.WriteAllTextAsync(_usersIndexPath, json);
}
}

View File

@@ -0,0 +1,78 @@
using System;
using System.IO;
using System.Threading.Tasks;
using InstaArchive.Models;
namespace InstaArchive.Services;
public class FileSystemService
{
private readonly SettingsService _settingsService;
public FileSystemService(SettingsService settingsService)
{
_settingsService = settingsService;
}
public string GetMediaPath(InstagramUser user, MediaType mediaType, DateTime? postedAt = null)
{
var settings = _settingsService.GetSettings();
var basePath = user.CustomBasePath ?? settings.BasePath;
var userPath = Path.Combine(basePath, user.UserId.ToString());
var typePath = mediaType switch
{
MediaType.Photo => Path.Combine(userPath, "Feed"),
MediaType.Video => Path.Combine(userPath, "Feed"),
MediaType.Story => Path.Combine(userPath, "Stories"),
MediaType.Reel => Path.Combine(userPath, "Reels"),
MediaType.Highlight => Path.Combine(userPath, "Highlights"),
_ => userPath
};
// Apply date subfolder if enabled
if (settings.EnableDateSubfolders && postedAt.HasValue)
{
var dateFolder = postedAt.Value.ToString(settings.DateFolderFormat);
typePath = Path.Combine(typePath, dateFolder);
}
Directory.CreateDirectory(typePath);
return typePath;
}
public string GenerateFileName(long mediaId, MediaType mediaType, string extension)
{
var prefix = mediaType switch
{
MediaType.Story => "story",
MediaType.Reel => "reel",
MediaType.Highlight => "highlight",
_ => "media"
};
return $"{prefix}_{mediaId}{extension}";
}
public async Task<bool> FileExistsAsync(string path)
{
return await Task.Run(() => File.Exists(path));
}
public async Task<long> GetFileSizeAsync(string path)
{
return await Task.Run(() => new FileInfo(path).Length);
}
public async Task EnsureDirectoryExistsAsync(string path)
{
await Task.Run(() => Directory.CreateDirectory(path));
}
public string GetUserPath(long userId, string? customBasePath = null)
{
var settings = _settingsService.GetSettings();
var basePath = customBasePath ?? settings.BasePath;
return Path.Combine(basePath, userId.ToString());
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace InstaArchive.Services;
public class InstagramSessionService
{
private readonly string _sessionPath;
private Dictionary<string, string> _cookies = new();
public event EventHandler<bool>? SessionStateChanged;
public InstagramSessionService()
{
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"InstaArchive"
);
Directory.CreateDirectory(appDataPath);
_sessionPath = Path.Combine(appDataPath, "session.json");
LoadSession();
}
public bool IsAuthenticated => _cookies.Count > 0 && _cookies.ContainsKey("sessionid");
public async Task SaveSessionAsync(string cookieHeader)
{
_cookies.Clear();
var cookies = cookieHeader.Split(';')
.Select(c => c.Trim())
.Where(c => !string.IsNullOrEmpty(c));
foreach (var cookie in cookies)
{
var parts = cookie.Split('=', 2);
if (parts.Length == 2)
{
_cookies[parts[0].Trim()] = parts[1].Trim();
}
}
var json = JsonConvert.SerializeObject(_cookies, Formatting.Indented);
await File.WriteAllTextAsync(_sessionPath, json);
SessionStateChanged?.Invoke(this, IsAuthenticated);
}
public async Task SaveCookiesFromWebView2(IEnumerable<KeyValuePair<string, string>> cookies)
{
_cookies.Clear();
foreach (var cookie in cookies)
{
_cookies[cookie.Key] = cookie.Value;
}
var json = JsonConvert.SerializeObject(_cookies, Formatting.Indented);
await File.WriteAllTextAsync(_sessionPath, json);
SessionStateChanged?.Invoke(this, IsAuthenticated);
}
public string GetCookieHeader()
{
return string.Join("; ", _cookies.Select(kvp => $"{kvp.Key}={kvp.Value}"));
}
public string? GetCookie(string name)
{
return _cookies.TryGetValue(name, out var value) ? value : null;
}
public async Task ClearSessionAsync()
{
_cookies.Clear();
if (File.Exists(_sessionPath))
{
File.Delete(_sessionPath);
}
SessionStateChanged?.Invoke(this, false);
await Task.CompletedTask;
}
private void LoadSession()
{
try
{
if (File.Exists(_sessionPath))
{
var json = File.ReadAllText(_sessionPath);
var loaded = JsonConvert.DeserializeObject<Dictionary<string, string>>(json);
if (loaded != null)
{
_cookies = loaded;
}
}
}
catch
{
_cookies = new Dictionary<string, string>();
}
}
}

View File

@@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using InstaArchive.Models;
using InstaArchive.Repositories;
namespace InstaArchive.Services;
public class MediaDownloaderService
{
private readonly FileBasedMediaRepository _mediaRepository;
private readonly InstagramSessionService _sessionService;
private readonly FileSystemService _fileSystemService;
private readonly MetadataInjectionService _metadataService;
private readonly SettingsService _settingsService;
private readonly HttpClient _httpClient;
private readonly SemaphoreSlim _rateLimitSemaphore;
private readonly Queue<DateTime> _requestHistory = new();
public event EventHandler<LogEntry>? LogGenerated;
public MediaDownloaderService(
FileBasedMediaRepository mediaRepository,
InstagramSessionService sessionService,
FileSystemService fileSystemService,
MetadataInjectionService metadataService,
SettingsService settingsService)
{
_mediaRepository = mediaRepository;
_sessionService = sessionService;
_fileSystemService = fileSystemService;
_metadataService = metadataService;
_settingsService = settingsService;
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
var settings = _settingsService.GetSettings();
_rateLimitSemaphore = new SemaphoreSlim(settings.MaxConcurrentDownloads);
}
public async Task<bool> DownloadMediaAsync(
InstagramUser user,
long mediaId,
string mediaUrl,
MediaType mediaType,
DateTime? postedAt = null,
string? caption = null,
string? location = null,
double? latitude = null,
double? longitude = null)
{
try
{
// Check if already downloaded
var exists = await _mediaRepository.MediaExistsAsync(mediaId);
if (exists)
{
LogInfo($"Media {mediaId} already downloaded, skipping");
return false;
}
// Apply rate limiting
await ApplyRateLimitAsync();
// Download with retry logic
var fileBytes = await DownloadWithRetryAsync(mediaUrl);
if (fileBytes == null)
{
LogError($"Failed to download media {mediaId}");
return false;
}
// Determine file extension
var extension = GetExtensionFromUrl(mediaUrl) ??
(mediaType == MediaType.Video || mediaType == MediaType.Reel ? ".mp4" : ".jpg");
// Generate file path
var directory = _fileSystemService.GetMediaPath(user, mediaType, postedAt);
var fileName = _fileSystemService.GenerateFileName(mediaId, mediaType, extension);
var filePath = Path.Combine(directory, fileName);
// Save file
await File.WriteAllBytesAsync(filePath, fileBytes);
// Inject metadata if enabled and supported
var settings = _settingsService.GetSettings();
if (settings.EnableMetadataInjection &&
(extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) ||
extension.Equals(".png", StringComparison.OrdinalIgnoreCase)))
{
await _metadataService.InjectMetadataAsync(
filePath, caption, postedAt, location, latitude, longitude);
}
// Save to media index
var mediaItem = new MediaItem
{
InstagramMediaId = mediaId,
UserId = user.UserId,
MediaType = mediaType,
FileName = fileName,
LocalPath = filePath,
DownloadedAt = DateTime.UtcNow,
PostedAt = postedAt,
Caption = caption,
Location = location,
Latitude = latitude,
Longitude = longitude,
FileSize = fileBytes.Length,
Url = mediaUrl
};
await _mediaRepository.AddMediaAsync(mediaItem);
LogSuccess($"Downloaded {mediaType} {mediaId} for user {user.CurrentUsername}");
return true;
}
catch (Exception ex)
{
LogError($"Error downloading media {mediaId}: {ex.Message}");
return false;
}
}
private async Task<byte[]?> DownloadWithRetryAsync(string url)
{
var settings = _settingsService.GetSettings();
var maxAttempts = settings.BackoffMaxAttempts;
var baseDelay = settings.BackoffBaseDelaySeconds;
for (int attempt = 0; attempt < maxAttempts; attempt++)
{
try
{
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
var cookieHeader = _sessionService.GetCookieHeader();
if (!string.IsNullOrEmpty(cookieHeader))
{
_httpClient.DefaultRequestHeaders.Add("Cookie", cookieHeader);
}
var response = await _httpClient.GetAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
var delay = TimeSpan.FromSeconds(baseDelay * Math.Pow(2, attempt));
LogWarning($"Rate limited, waiting {delay.TotalSeconds}s before retry {attempt + 1}/{maxAttempts}");
await Task.Delay(delay);
continue;
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
catch (HttpRequestException ex)
{
if (attempt == maxAttempts - 1)
{
LogError($"Download failed after {maxAttempts} attempts: {ex.Message}");
return null;
}
var delay = TimeSpan.FromSeconds(baseDelay * Math.Pow(2, attempt));
await Task.Delay(delay);
}
}
return null;
}
private async Task ApplyRateLimitAsync()
{
var settings = _settingsService.GetSettings();
if (!settings.EnableRateLimiting)
{
return;
}
await _rateLimitSemaphore.WaitAsync();
try
{
var now = DateTime.UtcNow;
var oneHourAgo = now.AddHours(-1);
// Remove old requests
while (_requestHistory.Count > 0 && _requestHistory.Peek() < oneHourAgo)
{
_requestHistory.Dequeue();
}
// Check if we've hit the limit
if (_requestHistory.Count >= settings.RateLimitRequestsPerHour)
{
var oldestRequest = _requestHistory.Peek();
var waitTime = oldestRequest.AddHours(1) - now;
if (waitTime > TimeSpan.Zero)
{
LogWarning($"Rate limit reached, waiting {waitTime.TotalSeconds:F0}s");
await Task.Delay(waitTime);
}
}
_requestHistory.Enqueue(now);
}
finally
{
_rateLimitSemaphore.Release();
}
}
private string? GetExtensionFromUrl(string url)
{
try
{
var uri = new Uri(url);
var path = uri.AbsolutePath;
var extension = Path.GetExtension(path);
if (!string.IsNullOrEmpty(extension))
{
return extension;
}
}
catch { }
return null;
}
private void LogInfo(string message)
{
LogGenerated?.Invoke(this, new LogEntry
{
Timestamp = DateTime.Now,
Level = LogLevel.Info,
Message = message
});
}
private void LogSuccess(string message)
{
LogGenerated?.Invoke(this, new LogEntry
{
Timestamp = DateTime.Now,
Level = LogLevel.Success,
Message = message
});
}
private void LogWarning(string message)
{
LogGenerated?.Invoke(this, new LogEntry
{
Timestamp = DateTime.Now,
Level = LogLevel.Warning,
Message = message
});
}
private void LogError(string message)
{
LogGenerated?.Invoke(this, new LogEntry
{
Timestamp = DateTime.Now,
Level = LogLevel.Error,
Message = message
});
}
}

View File

@@ -0,0 +1,110 @@
using System;
using System.Threading.Tasks;
using TagLib;
using TagLib.Image;
namespace InstaArchive.Services;
public class MetadataInjectionService
{
public async Task InjectMetadataAsync(
string filePath,
string? caption = null,
DateTime? postedAt = null,
string? location = null,
double? latitude = null,
double? longitude = null)
{
await Task.Run(() =>
{
try
{
using var file = TagLib.File.Create(filePath);
if (file.Tag is TagLib.Image.CombinedImageTag imageTag)
{
// Set caption as description/comment
if (!string.IsNullOrEmpty(caption))
{
imageTag.Comment = caption;
}
// Set date taken
if (postedAt.HasValue)
{
imageTag.DateTime = postedAt.Value;
if (imageTag.Exif != null)
{
imageTag.Exif.DateTime = postedAt.Value;
imageTag.Exif.DateTimeOriginal = postedAt.Value;
imageTag.Exif.DateTimeDigitized = postedAt.Value;
}
}
// Set GPS coordinates
if (latitude.HasValue && longitude.HasValue)
{
if (imageTag.Exif != null)
{
imageTag.Exif.Latitude = latitude.Value;
imageTag.Exif.Longitude = longitude.Value;
}
}
// Set location in XMP if available
if (!string.IsNullOrEmpty(location))
{
// TagLib# has limited XMP support, storing in keywords as fallback
imageTag.Keywords = new[] { $"Location:{location}" };
}
file.Save();
}
}
catch (Exception ex)
{
// Log error but don't fail the download
Console.WriteLine($"Failed to inject metadata: {ex.Message}");
}
});
}
public async Task<MediaMetadata?> ReadMetadataAsync(string filePath)
{
return await Task.Run(() =>
{
try
{
using var file = TagLib.File.Create(filePath);
if (file.Tag is TagLib.Image.CombinedImageTag imageTag)
{
return new MediaMetadata
{
Caption = imageTag.Comment,
DateTaken = imageTag.DateTime,
Latitude = imageTag.Exif?.Latitude,
Longitude = imageTag.Exif?.Longitude,
Keywords = imageTag.Keywords
};
}
}
catch
{
// Ignore errors
}
return null;
});
}
}
public class MediaMetadata
{
public string? Caption { get; set; }
public DateTime? DateTaken { get; set; }
public double? Latitude { get; set; }
public double? Longitude { get; set; }
public string[]? Keywords { get; set; }
}

View File

@@ -0,0 +1,257 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using InstaArchive.Models;
namespace InstaArchive.Services;
public class SchedulerService
{
private readonly UserManagementService _userManagementService;
private readonly MediaDownloaderService _mediaDownloaderService;
private readonly SettingsService _settingsService;
private readonly Dictionary<long, CancellationTokenSource> _userTimers = new();
private bool _isRunning;
public event EventHandler<LogEntry>? LogGenerated;
public event EventHandler<bool>? MonitoringStateChanged;
public bool IsMonitoring => _isRunning;
public SchedulerService(
UserManagementService userManagementService,
MediaDownloaderService mediaDownloaderService,
SettingsService settingsService)
{
_userManagementService = userManagementService;
_mediaDownloaderService = mediaDownloaderService;
_settingsService = settingsService;
_mediaDownloaderService.LogGenerated += (s, log) => LogGenerated?.Invoke(s, log);
}
public async Task StartMonitoringAsync()
{
if (_isRunning)
{
return;
}
_isRunning = true;
MonitoringStateChanged?.Invoke(this, true);
LogInfo("Monitoring started");
var users = await _userManagementService.GetAllUsersAsync();
foreach (var user in users)
{
await StartUserMonitoringAsync(user);
}
}
public async Task StopMonitoringAsync()
{
if (!_isRunning)
{
return;
}
_isRunning = false;
foreach (var cts in _userTimers.Values)
{
cts.Cancel();
}
_userTimers.Clear();
MonitoringStateChanged?.Invoke(this, false);
LogInfo("Monitoring stopped");
await Task.CompletedTask;
}
public async Task StartUserMonitoringAsync(InstagramUser user)
{
if (!_isRunning)
{
return;
}
if (_userTimers.ContainsKey(user.UserId))
{
return; // Already monitoring
}
var cts = new CancellationTokenSource();
_userTimers[user.UserId] = cts;
// Start monitoring tasks
if (user.MonitorStories)
{
_ = MonitorStoriesAsync(user, cts.Token);
}
if (user.MonitorPosts || user.MonitorReels)
{
_ = MonitorFeedAsync(user, cts.Token);
}
if (user.MonitorHighlights)
{
_ = MonitorHighlightsAsync(user, cts.Token);
}
LogInfo($"Started monitoring user {user.CurrentUsername} (ID: {user.UserId})");
await Task.CompletedTask;
}
public async Task StopUserMonitoringAsync(long userId)
{
if (_userTimers.TryGetValue(userId, out var cts))
{
cts.Cancel();
_userTimers.Remove(userId);
var user = await _userManagementService.GetUserAsync(userId);
if (user != null)
{
LogInfo($"Stopped monitoring user {user.CurrentUsername} (ID: {userId})");
}
}
}
private async Task MonitorStoriesAsync(InstagramUser user, CancellationToken cancellationToken)
{
var interval = TimeSpan.FromMinutes(user.StoriesCheckInterval);
while (!cancellationToken.IsCancellationRequested)
{
try
{
await CheckStoriesAsync(user);
}
catch (Exception ex)
{
LogError($"Error checking stories for {user.CurrentUsername}: {ex.Message}");
}
try
{
await Task.Delay(interval, cancellationToken);
}
catch (TaskCanceledException)
{
break;
}
}
}
private async Task MonitorFeedAsync(InstagramUser user, CancellationToken cancellationToken)
{
var interval = TimeSpan.FromMinutes(user.PostsCheckInterval);
while (!cancellationToken.IsCancellationRequested)
{
try
{
await CheckFeedAsync(user);
}
catch (Exception ex)
{
LogError($"Error checking feed for {user.CurrentUsername}: {ex.Message}");
}
try
{
await Task.Delay(interval, cancellationToken);
}
catch (TaskCanceledException)
{
break;
}
}
}
private async Task MonitorHighlightsAsync(InstagramUser user, CancellationToken cancellationToken)
{
var interval = TimeSpan.FromHours(24); // Check once daily
while (!cancellationToken.IsCancellationRequested)
{
try
{
await CheckHighlightsAsync(user);
}
catch (Exception ex)
{
LogError($"Error checking highlights for {user.CurrentUsername}: {ex.Message}");
}
try
{
await Task.Delay(interval, cancellationToken);
}
catch (TaskCanceledException)
{
break;
}
}
}
private async Task CheckStoriesAsync(InstagramUser user)
{
// This is a placeholder - actual Instagram API integration would go here
// In a real implementation, you would:
// 1. Fetch stories from Instagram API
// 2. For each story, call _mediaDownloaderService.DownloadMediaAsync()
LogInfo($"Checking stories for {user.CurrentUsername}...");
await Task.CompletedTask;
}
private async Task CheckFeedAsync(InstagramUser user)
{
// This is a placeholder - actual Instagram API integration would go here
// In a real implementation, you would:
// 1. Fetch recent posts from Instagram API
// 2. For each post/reel, call _mediaDownloaderService.DownloadMediaAsync()
LogInfo($"Checking feed for {user.CurrentUsername}...");
await Task.CompletedTask;
}
private async Task CheckHighlightsAsync(InstagramUser user)
{
// This is a placeholder - actual Instagram API integration would go here
// In a real implementation, you would:
// 1. Fetch highlights from Instagram API
// 2. For each highlight story, call _mediaDownloaderService.DownloadMediaAsync()
LogInfo($"Checking highlights for {user.CurrentUsername}...");
await Task.CompletedTask;
}
private void LogInfo(string message)
{
LogGenerated?.Invoke(this, new LogEntry
{
Timestamp = DateTime.Now,
Level = LogLevel.Info,
Message = message
});
}
private void LogError(string message)
{
LogGenerated?.Invoke(this, new LogEntry
{
Timestamp = DateTime.Now,
Level = LogLevel.Error,
Message = message
});
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Newtonsoft.Json;
using InstaArchive.Models;
namespace InstaArchive.Services;
public class SettingsService
{
private readonly string _settingsPath;
private AppSettings _settings;
public event EventHandler<AppSettings>? SettingsChanged;
public SettingsService()
{
var appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"InstaArchive"
);
Directory.CreateDirectory(appDataPath);
_settingsPath = Path.Combine(appDataPath, "app_settings.json");
_settings = new AppSettings();
LoadSettings();
}
public AppSettings GetSettings() => _settings;
public async Task SaveSettingsAsync(AppSettings settings)
{
_settings = settings;
var json = JsonConvert.SerializeObject(settings, Formatting.Indented);
await File.WriteAllTextAsync(_settingsPath, json);
SettingsChanged?.Invoke(this, settings);
}
private void LoadSettings()
{
try
{
if (File.Exists(_settingsPath))
{
var json = File.ReadAllText(_settingsPath);
var loaded = JsonConvert.DeserializeObject<AppSettings>(json);
if (loaded != null)
{
_settings = loaded;
}
}
else
{
// Save default settings
Task.Run(() => SaveSettingsAsync(_settings)).Wait();
}
}
catch
{
_settings = new AppSettings();
}
}
public async Task<string> ExportSettingsAsync(string exportPath)
{
var json = JsonConvert.SerializeObject(_settings, Formatting.Indented);
await File.WriteAllTextAsync(exportPath, json);
return exportPath;
}
public async Task ImportSettingsAsync(string importPath)
{
var json = await File.ReadAllTextAsync(importPath);
var imported = JsonConvert.DeserializeObject<AppSettings>(json);
if (imported != null)
{
await SaveSettingsAsync(imported);
}
}
}

View File

@@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Newtonsoft.Json;
using InstaArchive.Models;
using InstaArchive.Repositories;
namespace InstaArchive.Services;
public class UserManagementService
{
private readonly FileBasedUserRepository _userRepository;
private readonly SettingsService _settingsService;
public UserManagementService(FileBasedUserRepository userRepository, SettingsService settingsService)
{
_userRepository = userRepository;
_settingsService = settingsService;
}
public async Task<InstagramUser> AddUserAsync(long userId, string username)
{
var user = new InstagramUser
{
UserId = userId,
CurrentUsername = username,
UsernameHistoryJson = JsonConvert.SerializeObject(new List<UsernameHistoryEntry>
{
new() { Username = username, ChangedAt = DateTime.UtcNow }
})
};
await _userRepository.AddUserAsync(user);
await CreateUserDirectoryStructureAsync(user);
return user;
}
public async Task UpdateUserAsync(InstagramUser user)
{
var existing = await _userRepository.GetUserByIdAsync(user.UserId);
if (existing == null)
{
throw new InvalidOperationException($"User with ID {user.UserId} not found");
}
// Check if username changed
if (existing.CurrentUsername != user.CurrentUsername)
{
var history = JsonConvert.DeserializeObject<List<UsernameHistoryEntry>>(
existing.UsernameHistoryJson
) ?? new List<UsernameHistoryEntry>();
history.Add(new UsernameHistoryEntry
{
Username = user.CurrentUsername,
ChangedAt = DateTime.UtcNow
});
user.UsernameHistoryJson = JsonConvert.SerializeObject(history);
}
await _userRepository.UpdateUserAsync(user);
await SaveUserMetadataAsync(user);
}
public async Task<InstagramUser?> GetUserAsync(long userId)
{
return await _userRepository.GetUserByIdAsync(userId);
}
public async Task<List<InstagramUser>> GetAllUsersAsync()
{
return await _userRepository.GetAllUsersAsync();
}
public async Task DeleteUserAsync(long userId)
{
await _userRepository.DeleteUserAsync(userId);
}
private async Task CreateUserDirectoryStructureAsync(InstagramUser user)
{
var settings = _settingsService.GetSettings();
var basePath = user.CustomBasePath ?? settings.BasePath;
var userPath = Path.Combine(basePath, user.UserId.ToString());
Directory.CreateDirectory(userPath);
Directory.CreateDirectory(Path.Combine(userPath, "Feed"));
Directory.CreateDirectory(Path.Combine(userPath, "Stories"));
Directory.CreateDirectory(Path.Combine(userPath, "Reels"));
Directory.CreateDirectory(Path.Combine(userPath, "Highlights"));
await SaveUserMetadataAsync(user);
}
private async Task SaveUserMetadataAsync(InstagramUser user)
{
var settings = _settingsService.GetSettings();
var basePath = user.CustomBasePath ?? settings.BasePath;
var userPath = Path.Combine(basePath, user.UserId.ToString());
var metadataPath = Path.Combine(userPath, "user_metadata.json");
var metadata = new
{
user.UserId,
user.CurrentUsername,
user.Biography,
user.ProfilePictureUrl,
user.AddedDate,
user.LastUpdated,
UsernameHistory = JsonConvert.DeserializeObject<List<UsernameHistoryEntry>>(
user.UsernameHistoryJson
),
Configuration = new
{
user.MonitorPosts,
user.MonitorStories,
user.MonitorReels,
user.MonitorHighlights,
user.StoriesCheckInterval,
user.PostsCheckInterval,
user.CustomBasePath
}
};
var json = JsonConvert.SerializeObject(metadata, Formatting.Indented);
await File.WriteAllTextAsync(metadataPath, json);
}
public async Task<string> ExportUsersAsync(string exportPath)
{
var users = await GetAllUsersAsync();
var json = JsonConvert.SerializeObject(users, Formatting.Indented);
await File.WriteAllTextAsync(exportPath, json);
return exportPath;
}
public async Task ImportUsersAsync(string importPath)
{
var json = await File.ReadAllTextAsync(importPath);
var users = JsonConvert.DeserializeObject<List<InstagramUser>>(json);
if (users != null)
{
foreach (var user in users)
{
var existing = await _userRepository.GetUserByIdAsync(user.UserId);
if (existing == null)
{
await _userRepository.AddUserAsync(user);
await CreateUserDirectoryStructureAsync(user);
}
}
}
}
}

View File

@@ -0,0 +1,127 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using InstaArchive.Models;
using InstaArchive.Services;
using InstaArchive.Repositories;
namespace InstaArchive.ViewModels;
public partial class DashboardViewModel : ObservableObject
{
private readonly FileBasedMediaRepository _mediaRepository;
private readonly FileBasedUserRepository _userRepository;
private readonly SchedulerService _schedulerService;
private readonly UserManagementService _userManagementService;
[ObservableProperty]
private int totalMediaCount;
[ObservableProperty]
private int activeTargetsCount;
[ObservableProperty]
private int todayDownloadsCount;
[ObservableProperty]
private string storageUsed = "0 MB";
[ObservableProperty]
private bool isMonitoring;
[ObservableProperty]
private ObservableCollection<LogEntry> recentLogs = new();
[ObservableProperty]
private ObservableCollection<MediaItem> recentMedia = new();
public DashboardViewModel(
FileBasedMediaRepository mediaRepository,
FileBasedUserRepository userRepository,
SchedulerService schedulerService,
UserManagementService userManagementService)
{
_mediaRepository = mediaRepository;
_userRepository = userRepository;
_schedulerService = schedulerService;
_userManagementService = userManagementService;
_schedulerService.LogGenerated += OnLogGenerated;
_schedulerService.MonitoringStateChanged += OnMonitoringStateChanged;
_ = LoadDataAsync();
}
private void OnLogGenerated(object? sender, LogEntry log)
{
RecentLogs.Insert(0, log);
if (RecentLogs.Count > 100)
{
RecentLogs.RemoveAt(RecentLogs.Count - 1);
}
}
private void OnMonitoringStateChanged(object? sender, bool isMonitoring)
{
IsMonitoring = isMonitoring;
}
[RelayCommand]
private async Task StartMonitoringAsync()
{
await _schedulerService.StartMonitoringAsync();
}
[RelayCommand]
private async Task StopMonitoringAsync()
{
await _schedulerService.StopMonitoringAsync();
}
[RelayCommand]
private async Task RefreshAsync()
{
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
TotalMediaCount = await _mediaRepository.GetTotalMediaCountAsync();
var users = await _userRepository.GetAllUsersAsync();
ActiveTargetsCount = users.Count;
TodayDownloadsCount = await _mediaRepository.GetTodayDownloadsCountAsync();
var totalSize = await _mediaRepository.GetTotalStorageUsedAsync();
StorageUsed = FormatFileSize(totalSize);
var recent = await _mediaRepository.GetRecentMediaAsync(20);
RecentMedia.Clear();
foreach (var item in recent)
{
RecentMedia.Add(item);
}
IsMonitoring = _schedulerService.IsMonitoring;
}
private string FormatFileSize(long bytes)
{
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
double len = bytes;
int order = 0;
while (len >= 1024 && order < sizes.Length - 1)
{
order++;
len = len / 1024;
}
return $"{len:0.##} {sizes[order]}";
}
}

View File

@@ -0,0 +1,164 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Threading.Tasks;
using InstaArchive.Models;
using InstaArchive.Services;
using Windows.Storage.Pickers;
using Windows.Storage;
using System;
namespace InstaArchive.ViewModels;
public partial class SettingsViewModel : ObservableObject
{
private readonly SettingsService _settingsService;
private readonly UserManagementService _userManagementService;
[ObservableProperty]
private AppSettings settings;
public SettingsViewModel(
SettingsService settingsService,
UserManagementService userManagementService)
{
_settingsService = settingsService;
_userManagementService = userManagementService;
settings = _settingsService.GetSettings();
}
[RelayCommand]
private async Task SaveAsync()
{
await _settingsService.SaveSettingsAsync(Settings);
}
[RelayCommand]
private async Task BrowsePathAsync()
{
try
{
var picker = new FolderPicker();
// Get the current window's HWND
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
picker.FileTypeFilter.Add("*");
var folder = await picker.PickSingleFolderAsync();
if (folder != null)
{
Settings.BasePath = folder.Path;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Folder picker error: {ex.Message}");
}
}
[RelayCommand]
private async Task ExportSettingsAsync()
{
try
{
var picker = new FileSavePicker();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
picker.SuggestedStartLocation = PickerLocationId.Desktop;
picker.SuggestedFileName = "instaarchive_settings";
picker.FileTypeChoices.Add("JSON File", new[] { ".json" });
var file = await picker.PickSaveFileAsync();
if (file != null)
{
await _settingsService.ExportSettingsAsync(file.Path);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Export settings error: {ex.Message}");
}
}
[RelayCommand]
private async Task ExportTargetsAsync()
{
try
{
var picker = new FileSavePicker();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
picker.SuggestedStartLocation = PickerLocationId.Desktop;
picker.SuggestedFileName = "instaarchive_targets";
picker.FileTypeChoices.Add("JSON File", new[] { ".json" });
var file = await picker.PickSaveFileAsync();
if (file != null)
{
await _userManagementService.ExportUsersAsync(file.Path);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Export targets error: {ex.Message}");
}
}
[RelayCommand]
private async Task ImportSettingsAsync()
{
try
{
var picker = new FileOpenPicker();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
picker.SuggestedStartLocation = PickerLocationId.Desktop;
picker.FileTypeFilter.Add(".json");
var file = await picker.PickSingleFileAsync();
if (file != null)
{
await _settingsService.ImportSettingsAsync(file.Path);
Settings = _settingsService.GetSettings();
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Import settings error: {ex.Message}");
}
}
[RelayCommand]
private async Task ImportTargetsAsync()
{
try
{
var picker = new FileOpenPicker();
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(App.MainWindow);
WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd);
picker.SuggestedStartLocation = PickerLocationId.Desktop;
picker.FileTypeFilter.Add(".json");
var file = await picker.PickSingleFileAsync();
if (file != null)
{
await _userManagementService.ImportUsersAsync(file.Path);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Import targets error: {ex.Message}");
}
}
}

View File

@@ -0,0 +1,146 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using InstaArchive.Models;
using InstaArchive.Services;
namespace InstaArchive.ViewModels;
public partial class TargetsViewModel : ObservableObject
{
private readonly UserManagementService _userManagementService;
private readonly SchedulerService _schedulerService;
[ObservableProperty]
private ObservableCollection<InstagramUser> users = new();
[ObservableProperty]
private ObservableCollection<InstagramUser> filteredUsers = new();
[ObservableProperty]
private InstagramUser? selectedUser;
[ObservableProperty]
private long newUserId;
[ObservableProperty]
private string newUsername = string.Empty;
[ObservableProperty]
private string searchQuery = string.Empty;
public TargetsViewModel(
UserManagementService userManagementService,
SchedulerService schedulerService)
{
_userManagementService = userManagementService;
_schedulerService = schedulerService;
_ = LoadUsersAsync();
}
partial void OnSearchQueryChanged(string value)
{
FilterUsers();
}
private void FilterUsers()
{
if (string.IsNullOrWhiteSpace(SearchQuery))
{
FilteredUsers.Clear();
foreach (var user in Users)
{
FilteredUsers.Add(user);
}
}
else
{
var query = SearchQuery.ToLower().Trim();
var filtered = Users.Where(u =>
u.CurrentUsername.ToLower().Contains(query) ||
u.UserId.ToString().Contains(query)
).ToList();
FilteredUsers.Clear();
foreach (var user in filtered)
{
FilteredUsers.Add(user);
}
}
}
[RelayCommand]
private async Task AddUserAsync()
{
if (NewUserId <= 0 || string.IsNullOrWhiteSpace(NewUsername))
{
return;
}
try
{
var user = await _userManagementService.AddUserAsync(NewUserId, NewUsername);
Users.Add(user);
FilterUsers();
if (_schedulerService.IsMonitoring)
{
await _schedulerService.StartUserMonitoringAsync(user);
}
NewUserId = 0;
NewUsername = string.Empty;
}
catch
{
// Show error to user
}
}
[RelayCommand]
private async Task DeleteUserAsync(InstagramUser user)
{
if (user == null)
{
return;
}
await _schedulerService.StopUserMonitoringAsync(user.UserId);
await _userManagementService.DeleteUserAsync(user.UserId);
Users.Remove(user);
FilterUsers();
}
[RelayCommand]
private async Task SaveUserAsync()
{
if (SelectedUser == null)
{
return;
}
await _userManagementService.UpdateUserAsync(SelectedUser);
}
[RelayCommand]
private async Task RefreshAsync()
{
await LoadUsersAsync();
}
private async Task LoadUsersAsync()
{
var userList = await _userManagementService.GetAllUsersAsync();
Users.Clear();
foreach (var user in userList)
{
Users.Add(user);
}
FilterUsers();
}
}

View File

@@ -0,0 +1,157 @@
<Page
x:Class="InstaArchive.Views.DashboardPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ScrollViewer>
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Header -->
<Grid Grid.Row="0" Margin="0,0,0,24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Pannello di Controllo"
Style="{ThemeResource TitleTextBlockStyle}"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="12">
<Button Content="Avvia Monitoraggio"
Command="{x:Bind ViewModel.StartMonitoringCommand}"
Visibility="{x:Bind ViewModel.IsMonitoring, Mode=OneWay, Converter={StaticResource InverseBoolToVisibilityConverter}}"/>
<Button Content="Ferma Monitoraggio"
Command="{x:Bind ViewModel.StopMonitoringCommand}"
Visibility="{x:Bind ViewModel.IsMonitoring, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"/>
<Button Command="{x:Bind ViewModel.RefreshCommand}">
<SymbolIcon Symbol="Refresh"/>
</Button>
</StackPanel>
</Grid>
<!-- Stats Ribbon -->
<Grid Grid.Row="1" Margin="0,0,0,24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8" Padding="20" Margin="0,0,12,0">
<StackPanel>
<TextBlock Text="Media Totali"
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<TextBlock Text="{x:Bind ViewModel.TotalMediaCount, Mode=OneWay}"
Style="{ThemeResource TitleTextBlockStyle}"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
<Border Grid.Column="1" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8" Padding="20" Margin="0,0,12,0">
<StackPanel>
<TextBlock Text="Obiettivi Attivi"
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<TextBlock Text="{x:Bind ViewModel.ActiveTargetsCount, Mode=OneWay}"
Style="{ThemeResource TitleTextBlockStyle}"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
<Border Grid.Column="2" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8" Padding="20" Margin="0,0,12,0">
<StackPanel>
<TextBlock Text="Download Oggi"
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<TextBlock Text="{x:Bind ViewModel.TodayDownloadsCount, Mode=OneWay}"
Style="{ThemeResource TitleTextBlockStyle}"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
<Border Grid.Column="3" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8" Padding="20">
<StackPanel>
<TextBlock Text="Spazio Utilizzato"
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<TextBlock Text="{x:Bind ViewModel.StorageUsed, Mode=OneWay}"
Style="{ThemeResource TitleTextBlockStyle}"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
</Grid>
<!-- Recent Activity -->
<Border Grid.Row="2" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8" Padding="20" Margin="0,0,0,24">
<StackPanel>
<TextBlock Text="Attività Recenti"
Style="{ThemeResource SubtitleTextBlockStyle}"
Margin="0,0,0,12"/>
<GridView ItemsSource="{x:Bind ViewModel.RecentMedia, Mode=OneWay}"
SelectionMode="None"
IsItemClickEnabled="False">
<GridView.ItemTemplate>
<DataTemplate>
<Border Width="120" Height="120"
Background="{ThemeResource LayerFillColorDefaultBrush}"
CornerRadius="4">
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<SymbolIcon Symbol="Pictures" />
<TextBlock Text="{Binding MediaType}"
Style="{ThemeResource CaptionTextBlockStyle}"
HorizontalAlignment="Center"
Margin="0,4,0,0"/>
</StackPanel>
</Border>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</StackPanel>
</Border>
<!-- Log Console -->
<Border Grid.Row="3" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8" Padding="20">
<StackPanel>
<TextBlock Text="Registro Attività"
Style="{ThemeResource SubtitleTextBlockStyle}"
Margin="0,0,0,12"/>
<ScrollViewer MaxHeight="300">
<ItemsControl ItemsSource="{x:Bind ViewModel.RecentLogs, Mode=OneWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="0,4">
<TextBlock>
<Run Text="{Binding Timestamp, Converter={StaticResource TimeFormatConverter}}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<Run Text=" - "/>
<Run Text="{Binding Message}"/>
</TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</Border>
</Grid>
</ScrollViewer>
</Page>

View File

@@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using InstaArchive.ViewModels;
namespace InstaArchive.Views;
public sealed partial class DashboardPage : Page
{
public DashboardViewModel ViewModel { get; }
public DashboardPage()
{
InitializeComponent();
ViewModel = App.Services.GetRequiredService<DashboardViewModel>();
}
}

View File

@@ -0,0 +1,159 @@
<Page
x:Class="InstaArchive.Views.SettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<ScrollViewer>
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Impostazioni"
Style="{ThemeResource TitleTextBlockStyle}"
Margin="0,0,0,24"/>
<StackPanel Grid.Row="1" Spacing="16" MaxWidth="800" HorizontalAlignment="Left">
<!-- Storage Settings -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="16">
<TextBlock Text="Configurazione Archiviazione"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Header="Percorso Base"
Text="{x:Bind ViewModel.Settings.BasePath, Mode=TwoWay}"
Grid.Column="0"
Margin="0,0,8,0"/>
<Button Content="Sfoglia"
Command="{x:Bind ViewModel.BrowsePathCommand}"
Grid.Column="1"
VerticalAlignment="Bottom"/>
</Grid>
<ToggleSwitch Header="Abilita Sottocartelle per Data"
IsOn="{x:Bind ViewModel.Settings.EnableDateSubfolders, Mode=TwoWay}"/>
<TextBox Header="Formato Cartella Data"
PlaceholderText="yyyy-MM-dd"
Text="{x:Bind ViewModel.Settings.DateFolderFormat, Mode=TwoWay}"/>
</StackPanel>
</Border>
<!-- Metadata Settings -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="16">
<TextBlock Text="Configurazione Metadati"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<ToggleSwitch Header="Abilita Iniezione Metadati"
IsOn="{x:Bind ViewModel.Settings.EnableMetadataInjection, Mode=TwoWay}"/>
</StackPanel>
</Border>
<!-- Download Settings -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="16">
<TextBlock Text="Configurazione Download"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<NumberBox Header="Massimo Download Concorrenti"
Value="{x:Bind ViewModel.Settings.MaxConcurrentDownloads, Mode=TwoWay}"
Minimum="1"
Maximum="10"/>
<NumberBox Header="Intervallo Globale Storie (minuti)"
Value="{x:Bind ViewModel.Settings.GlobalStoryCheckInterval, Mode=TwoWay}"
Minimum="1"
Maximum="1440"/>
<NumberBox Header="Intervallo Globale Post (minuti)"
Value="{x:Bind ViewModel.Settings.GlobalPostCheckInterval, Mode=TwoWay}"
Minimum="1"
Maximum="10080"/>
<ToggleSwitch Header="Avvia Automaticamente Monitoraggio"
IsOn="{x:Bind ViewModel.Settings.AutoStartMonitoring, Mode=TwoWay}"/>
</StackPanel>
</Border>
<!-- Rate Limiting -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="16">
<TextBlock Text="Limitazione Richieste"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<ToggleSwitch Header="Abilita Limitazione Richieste"
IsOn="{x:Bind ViewModel.Settings.EnableRateLimiting, Mode=TwoWay}"/>
<NumberBox Header="Massimo Richieste per Ora"
Value="{x:Bind ViewModel.Settings.RateLimitRequestsPerHour, Mode=TwoWay}"
Minimum="10"
Maximum="1000"/>
<NumberBox Header="Ritardo Base Backoff (secondi)"
Value="{x:Bind ViewModel.Settings.BackoffBaseDelaySeconds, Mode=TwoWay}"
Minimum="1"
Maximum="300"/>
<NumberBox Header="Massimo Tentativi Backoff"
Value="{x:Bind ViewModel.Settings.BackoffMaxAttempts, Mode=TwoWay}"
Minimum="1"
Maximum="10"/>
</StackPanel>
</Border>
<!-- Import/Export -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="16">
<TextBlock Text="Importa / Esporta"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<StackPanel Orientation="Horizontal" Spacing="12">
<Button Content="Esporta Impostazioni"
Command="{x:Bind ViewModel.ExportSettingsCommand}"/>
<Button Content="Importa Impostazioni"
Command="{x:Bind ViewModel.ImportSettingsCommand}"/>
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="12">
<Button Content="Esporta Obiettivi"
Command="{x:Bind ViewModel.ExportTargetsCommand}"/>
<Button Content="Importa Obiettivi"
Command="{x:Bind ViewModel.ImportTargetsCommand}"/>
</StackPanel>
</StackPanel>
</Border>
<!-- Save Button -->
<Button Content="Salva Impostazioni"
Command="{x:Bind ViewModel.SaveCommand}"
HorizontalAlignment="Right"
Style="{ThemeResource AccentButtonStyle}"/>
</StackPanel>
</Grid>
</ScrollViewer>
</Page>

View File

@@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using InstaArchive.ViewModels;
namespace InstaArchive.Views;
public sealed partial class SettingsPage : Page
{
public SettingsViewModel ViewModel { get; }
public SettingsPage()
{
InitializeComponent();
ViewModel = App.Services.GetRequiredService<SettingsViewModel>();
}
}

166
Teti/Views/TargetsPage.xaml Normal file
View File

@@ -0,0 +1,166 @@
<Page
x:Class="InstaArchive.Views.TargetsPage"
x:Name="TargetsPageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="using:InstaArchive.Models"
mc:Ignorable="d">
<Grid Margin="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="350"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- User List -->
<Grid Grid.Column="0" Margin="0,0,24,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Utenti Obiettivo"
Style="{ThemeResource TitleTextBlockStyle}"
Margin="0,0,0,24"/>
<!-- Add User Form -->
<Border Grid.Row="1"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="16"
Margin="0,0,0,16">
<StackPanel Spacing="12">
<TextBlock Text="Aggiungi Nuovo Obiettivo"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<TextBox Header="ID Utente Instagram"
PlaceholderText="Es. 123456789"
Text="{x:Bind ViewModel.NewUserId, Mode=TwoWay}"/>
<TextBox Header="Nome Utente"
PlaceholderText="Es. @nomeutente"
Text="{x:Bind ViewModel.NewUsername, Mode=TwoWay}"/>
<Button Content="Aggiungi Utente"
Command="{x:Bind ViewModel.AddUserCommand}"
HorizontalAlignment="Stretch"
Style="{ThemeResource AccentButtonStyle}"/>
</StackPanel>
</Border>
<!-- Search Box -->
<Border Grid.Row="2"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="16"
Margin="0,0,0,16">
<StackPanel Spacing="8">
<TextBlock Text="Cerca Utente"
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<TextBox PlaceholderText="Cerca per ID o nome utente..."
Text="{x:Bind ViewModel.SearchQuery, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</Border>
<!-- Users List -->
<ListView Grid.Row="3"
ItemsSource="{x:Bind ViewModel.FilteredUsers, Mode=OneWay}"
SelectedItem="{x:Bind ViewModel.SelectedUser, Mode=TwoWay}"
SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:InstagramUser">
<Grid Padding="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="{x:Bind CurrentUsername}"
Style="{ThemeResource BaseTextBlockStyle}"/>
<TextBlock Text="{x:Bind UserId}"
Style="{ThemeResource CaptionTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
</StackPanel>
<Button Grid.Column="1">
<SymbolIcon Symbol="Delete"/>
</Button>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
<!-- User Details -->
<ScrollViewer Grid.Column="1">
<StackPanel Spacing="16" Visibility="{x:Bind ViewModel.SelectedUser, Mode=OneWay, Converter={StaticResource NullToVisibilityConverter}}">
<TextBlock Text="Configurazione Obiettivo"
Style="{ThemeResource TitleTextBlockStyle}"/>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="16">
<TextBlock Text="Opzioni di Monitoraggio"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<ToggleSwitch Header="Monitora Post"
IsOn="{x:Bind ViewModel.SelectedUser.MonitorPosts, Mode=TwoWay}"/>
<ToggleSwitch Header="Monitora Storie"
IsOn="{x:Bind ViewModel.SelectedUser.MonitorStories, Mode=TwoWay}"/>
<ToggleSwitch Header="Monitora Reels"
IsOn="{x:Bind ViewModel.SelectedUser.MonitorReels, Mode=TwoWay}"/>
<ToggleSwitch Header="Monitora Highlights"
IsOn="{x:Bind ViewModel.SelectedUser.MonitorHighlights, Mode=TwoWay}"/>
</StackPanel>
</Border>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="16">
<TextBlock Text="Intervalli di Controllo"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<NumberBox Header="Intervallo Storie (minuti)"
Value="{x:Bind ViewModel.SelectedUser.StoriesCheckInterval, Mode=TwoWay}"
Minimum="1"
Maximum="1440"/>
<NumberBox Header="Intervallo Post (minuti)"
Value="{x:Bind ViewModel.SelectedUser.PostsCheckInterval, Mode=TwoWay}"
Minimum="1"
Maximum="10080"/>
</StackPanel>
</Border>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="20">
<StackPanel Spacing="16">
<TextBlock Text="Configurazione Percorso"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<TextBox Header="Percorso Base Personalizzato (Opzionale)"
PlaceholderText="Lascia vuoto per usare quello predefinito"
Text="{x:Bind ViewModel.SelectedUser.CustomBasePath, Mode=TwoWay}"/>
</StackPanel>
</Border>
<Button Content="Salva Modifiche"
Command="{x:Bind ViewModel.SaveUserCommand}"
HorizontalAlignment="Right"
Style="{ThemeResource AccentButtonStyle}"/>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using InstaArchive.ViewModels;
namespace InstaArchive.Views;
public sealed partial class TargetsPage : Page
{
public TargetsViewModel ViewModel { get; }
public TargetsPage()
{
InitializeComponent();
ViewModel = App.Services.GetRequiredService<TargetsViewModel>();
}
}

18
Teti/app.manifest Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Teti.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 and Windows 11 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>