diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0ed047f..cca27f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -288,7 +288,7 @@ jobs: env: ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - - name: Build Android ARM64 + - name: Build Android ARM64 APK run: | dotnet publish src/StripWolf/StripWolf.Android/StripWolf.Android.csproj -c Release -r android-arm64 -o publish/android-arm64 \ -p:DebugType=none -p:DebugSymbols=false -p:AllowedReferenceRelatedFileExtensions=none \ @@ -299,7 +299,7 @@ jobs: ANDROID_SIGNING_KEY_PASS: ${{ secrets.ANDROID_SIGNING_KEY_PASS || 'android' }} ANDROID_SIGNING_STORE_PASS: ${{ secrets.ANDROID_SIGNING_STORE_PASS || 'android' }} - - name: Build Android x64 + - name: Build Android x64 APK run: | dotnet publish src/StripWolf/StripWolf.Android/StripWolf.Android.csproj -c Release -r android-x64 -o publish/android-x64 \ -p:DebugType=none -p:DebugSymbols=false -p:AllowedReferenceRelatedFileExtensions=none \ @@ -310,27 +310,59 @@ jobs: ANDROID_SIGNING_KEY_PASS: ${{ secrets.ANDROID_SIGNING_KEY_PASS || 'android' }} ANDROID_SIGNING_STORE_PASS: ${{ secrets.ANDROID_SIGNING_STORE_PASS || 'android' }} - - name: Find and rename APKs + - name: Build Android App Bundle (AAB) + run: | + dotnet publish src/StripWolf/StripWolf.Android/StripWolf.Android.csproj -c Release -o publish/android-aab \ + -p:DebugType=none -p:DebugSymbols=false -p:AllowedReferenceRelatedFileExtensions=none \ + -p:AndroidPackageFormat=aab \ + -p:AndroidCreatePackagePerAbi=false \ + -p:IsPlayStoreBuild=true \ + -p:RuntimeIdentifiers="android-arm64;android-x64" \ + -p:GitVersion_CommitsSinceVersionSource=${{ steps.gitversion.outputs.commitsSinceVersionSource }} \ + -p:GitVersion_SemVer=${{ steps.gitversion.outputs.semVer }} + env: + ANDROID_SIGNING_KEY_PASS: ${{ secrets.ANDROID_SIGNING_KEY_PASS || 'android' }} + ANDROID_SIGNING_STORE_PASS: ${{ secrets.ANDROID_SIGNING_STORE_PASS || 'android' }} + + - name: Find and rename Android Artifacts run: | mkdir -p release-artifacts - # Process ARM64 + # Process ARM64 APK APK_ARM=$(find publish/android-arm64 -name "*-Signed.apk" | head -1) if [ -n "$APK_ARM" ]; then cp "$APK_ARM" "release-artifacts/StripWolf-Android-arm64-${{ needs.prepare.outputs.version }}-g${{ needs.prepare.outputs.hash }}.apk" fi - # Process x64 + # Process x64 APK APK_X64=$(find publish/android-x64 -name "*-Signed.apk" | head -1) if [ -n "$APK_X64" ]; then cp "$APK_X64" "release-artifacts/StripWolf-Android-x86_64-${{ needs.prepare.outputs.version }}-g${{ needs.prepare.outputs.hash }}.apk" fi + # Process AAB + AAB_FILE=$(find publish/android-aab -name "*-Signed.aab" | head -1) + if [ -n "$AAB_FILE" ]; then + cp "$AAB_FILE" "release-artifacts/StripWolf-Android-${{ needs.prepare.outputs.version }}-g${{ needs.prepare.outputs.hash }}.aab" + fi + - name: Upload to Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ needs.prepare.outputs.version }} - files: release-artifacts/*.apk + files: | + release-artifacts/*.apk + release-artifacts/*.aab draft: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload to Google Play + uses: r0adkll/upload-google-play@v2 + if: ${{ secrets.PLAY_SERVICE_ACCOUNT_KEY != '' }} + with: + serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_KEY }} + packageName: com.dapplo.StripWolf + releaseFiles: release-artifacts/*.aab + track: internal + status: completed diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 0000000..d988b41 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,30 @@ +# Privacy Policy for StripWolf + +**Effective Date: June 14, 2026** + +This privacy policy governs your use of the software application **StripWolf** ("Application") for mobile and desktop devices. StripWolf is a comic book reader that supports local files and self-hosted server integrations (such as Komga). + +## 1. Information Collection and Use +StripWolf is designed with a "local-first" privacy model. + +* **No Personal Data Collection**: We do not collect, harvest, or transmit any personally identifiable information (PII) or usage data to our servers. +* **Local Data Storage**: All library data, reading progress, login credentials for self-hosted servers, and personal reading statistics are stored strictly on your local device. +* **Self-Hosted Integrations**: If you connect the Application to a self-hosted server (e.g., Komga), your server address and credentials are encrypted and stored locally. Connections are made directly between your device and your server; we have no access to this traffic. + +## 2. In-App Purchases & Payments +The Application offers an optional premium unlock. +* Payment transactions are processed entirely and securely by Google Play Billing (Google Play Store). +* We do not collect, process, or store your payment details (such as credit card numbers or billing addresses). + +## 3. Data Backups +If you enable Android Auto Backup on your device, a copy of the Application's settings and credentials may be backed up to your personal Google Drive account. This backup data is encrypted and subject to Google's standard backup terms and privacy policies. + +## 4. Crash Logs & Issue Reporting +If you choose to submit a bug report via the "Report an Issue" button, the Application pre-populates a template containing: +* Your Operating System type and version (e.g. Android 13, Windows 10) +* The version of StripWolf you are running +No personal files, browsing history, or private data are attached. You will have full visibility of the report text before submitting it to our public GitHub issue tracker. + +## 5. Contact Us +If you have any questions regarding privacy while using the Application, please contact us via the GitHub project repository: +https://github.com/dapplo/StripWolf diff --git a/src/StripWolf/StripWolf.Android/Properties/AndroidManifest.xml b/src/StripWolf/StripWolf.Android/Properties/AndroidManifest.xml index c99d5fd..b99e9b0 100644 --- a/src/StripWolf/StripWolf.Android/Properties/AndroidManifest.xml +++ b/src/StripWolf/StripWolf.Android/Properties/AndroidManifest.xml @@ -3,5 +3,8 @@ + android:networkSecurityConfig="@xml/network_security_config" + android:allowBackup="true" + android:fullBackupContent="@xml/full_backup_content" + android:dataExtractionRules="@xml/backup_rules" /> diff --git a/src/StripWolf/StripWolf.Android/Resources/xml/backup_rules.xml b/src/StripWolf/StripWolf.Android/Resources/xml/backup_rules.xml new file mode 100644 index 0000000..a236fed --- /dev/null +++ b/src/StripWolf/StripWolf.Android/Resources/xml/backup_rules.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/src/StripWolf/StripWolf.Android/Resources/xml/full_backup_content.xml b/src/StripWolf/StripWolf.Android/Resources/xml/full_backup_content.xml new file mode 100644 index 0000000..46c7bca --- /dev/null +++ b/src/StripWolf/StripWolf.Android/Resources/xml/full_backup_content.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/StripWolf/StripWolf.Core/App.axaml.cs b/src/StripWolf/StripWolf.Core/App.axaml.cs index 07c4c86..5708314 100644 --- a/src/StripWolf/StripWolf.Core/App.axaml.cs +++ b/src/StripWolf/StripWolf.Core/App.axaml.cs @@ -63,6 +63,12 @@ public partial class App : Application /// public static Action? RegisterFullScreenService { get; set; } + /// + /// Action to register the platform-specific billing service. + /// Set this before Initialize() is called if you need a custom implementation (e.g., on Android). + /// + public static Action? RegisterBillingService { get; set; } + public override void Initialize() { AvaloniaXamlLoader.Load(this); @@ -183,6 +189,13 @@ private static void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + if (RegisterBillingService != null) + { + RegisterBillingService(services); + } // Register platform-specific PDF renderer // Use the custom registration action if set (e.g., for Android), otherwise default to nothing diff --git a/src/StripWolf/StripWolf.Core/Data/DatabaseService.cs b/src/StripWolf/StripWolf.Core/Data/DatabaseService.cs index 41a1a96..a2f4ce2 100644 --- a/src/StripWolf/StripWolf.Core/Data/DatabaseService.cs +++ b/src/StripWolf/StripWolf.Core/Data/DatabaseService.cs @@ -62,6 +62,9 @@ private static string GetAppDataDirectory() [DynamicDependency( DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties, typeof(KomgaPendingReadProgress))] + [DynamicDependency( + DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties, + typeof(UsageStats))] private async Task GetDatabaseAsync() { if (_isInitialized && _database is not null) @@ -89,6 +92,7 @@ private async Task GetDatabaseAsync() await _database.CreateTableAsync(); await _database.CreateTableAsync(); await _database.CreateTableAsync(); + await _database.CreateTableAsync(); await _database.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_Comic_FilePath ON Comic(FilePath)"); await _database.ExecuteAsync("CREATE INDEX IF NOT EXISTS IX_EpubConversionState_Status ON EpubConversionState(Status)"); @@ -469,6 +473,42 @@ await db.RunInTransactionAsync(connection => #endregion + #region Usage Stats + + public async Task LogUsageAsync(string metric, string? metadata = null) + { + try + { + var db = await GetDatabaseAsync(); + var stat = new UsageStats + { + Metric = metric, + Timestamp = DateTime.UtcNow, + Metadata = metadata + }; + await db.InsertAsync(stat); + } + catch + { + // Ignore stats errors so they don't crash the app + } + } + + public async Task GetUsageCountAsync(string metric) + { + try + { + var db = await GetDatabaseAsync(); + return await db.Table().Where(s => s.Metric == metric).CountAsync(); + } + catch + { + return 0; + } + } + + #endregion + public async ValueTask DisposeAsync() { if (_database is not null) diff --git a/src/StripWolf/StripWolf.Core/Models/AppSettings.cs b/src/StripWolf/StripWolf.Core/Models/AppSettings.cs index 85a08e4..9e5963c 100644 --- a/src/StripWolf/StripWolf.Core/Models/AppSettings.cs +++ b/src/StripWolf/StripWolf.Core/Models/AppSettings.cs @@ -61,6 +61,21 @@ public class AppSettings /// public bool? HasCompletedWelcomeExperience { get; set; } + /// + /// Whether the user has purchased the unlimited premium unlock. + /// + public bool IsUnlimitedUnlocked { get; set; } = false; + + /// + /// Book IDs of Komga comics that have been opened (Max 2 in trial). + /// + public List PermanentViewedKomgaBookIds { get; set; } = []; + + /// + /// Local comic paths/filenames that have been opened (Max 2 per format in trial). + /// + public List PermanentViewedLocalPaths { get; set; } = []; + /// /// Preferred reading mode for the comic reader /// @@ -189,6 +204,9 @@ public AppSettings Clone() WasInReader = WasInReader, LastTabIndex = LastTabIndex, HasCompletedWelcomeExperience = HasCompletedWelcomeExperience, + IsUnlimitedUnlocked = IsUnlimitedUnlocked, + PermanentViewedKomgaBookIds = PermanentViewedKomgaBookIds.ToList(), + PermanentViewedLocalPaths = PermanentViewedLocalPaths.ToList(), AppTheme = AppTheme, PreferredReadingMode = PreferredReadingMode, Handedness = Handedness, diff --git a/src/StripWolf/StripWolf.Core/Models/UsageStats.cs b/src/StripWolf/StripWolf.Core/Models/UsageStats.cs new file mode 100644 index 0000000..5cbf878 --- /dev/null +++ b/src/StripWolf/StripWolf.Core/Models/UsageStats.cs @@ -0,0 +1,20 @@ +using System; +using SQLite; + +namespace StripWolf.Core.Models; + +/// +/// Model for logging usage statistics in the database. +/// +public class UsageStats +{ + [PrimaryKey, AutoIncrement] + public int Id { get; set; } + + [Indexed] + public string Metric { get; set; } = string.Empty; // "KomgaDownload", "LocalImport", "ComicOpen", "PagesRead" + + public DateTime Timestamp { get; set; } = DateTime.UtcNow; + + public string? Metadata { get; set; } +} diff --git a/src/StripWolf/StripWolf.Core/Resources/Loc.cs b/src/StripWolf/StripWolf.Core/Resources/Loc.cs index dff6a58..5ca5a58 100644 --- a/src/StripWolf/StripWolf.Core/Resources/Loc.cs +++ b/src/StripWolf/StripWolf.Core/Resources/Loc.cs @@ -357,4 +357,31 @@ private string GetString([CallerMemberName] string? key = null) public string WelcomeExperienceActivityHint => GetString(); public string WelcomeExperienceSettingsHint => GetString(); public string WelcomeExperienceSupportHint => GetString(); + + // Google Play Store Trial limits and stats translations + public string TrialUnlockPremiumFeatures => GetString(); + public string TrialReachedLimitsDesc => GetString(); + public string TrialUnlimitedLocalImports => GetString(); + public string TrialUnlimitedKomga => GetString(); + public string TrialStatsDashboard => GetString(); + public string TrialSupportsDevelopment => GetString(); + public string TrialUnlockButton => GetString(); + public string TrialKeepTesting => GetString(); + public string TrialLimitReachedError => GetString(); + public string TrialEdition => GetString(); + public string TrialUpgradeDesc => GetString(); + public string TrialLocked => GetString(); + public string TrialKomgaLimitsDesc => GetString(); + public string TrialLocalLimitsTitle => GetString(); + public string TrialPremiumEdition => GetString(); + public string TrialAllFeaturesUnlocked => GetString(); + public string TrialUnlocked => GetString(); + public string TrialPersonalReadingStats => GetString(); + public string TrialLicenceAndStats => GetString(); + public string TrialViews => GetString(); + public string TrialPagesRead => GetString(); + public string TrialComicsOpened => GetString(); + public string TrialLocalImports => GetString(); + public string TrialKomgaDownloads => GetString(); + public string TrialKomgaServerIntegration => GetString(); } diff --git a/src/StripWolf/StripWolf.Core/Resources/Strings.de.resx b/src/StripWolf/StripWolf.Core/Resources/Strings.de.resx index 20ea345..20d458b 100644 --- a/src/StripWolf/StripWolf.Core/Resources/Strings.de.resx +++ b/src/StripWolf/StripWolf.Core/Resources/Strings.de.resx @@ -1,4 +1,4 @@ - + @@ -963,4 +963,79 @@ Fertig + + Sie haben einige Limits der Testversion erreicht. Bitte schalten Sie Premium frei, um diesen Comic zu lesen. + + + Persönliches Lese-Statistik-Dashboard + + + Geöffnete Comics + + + Persönliche Lesestatistik + + + Premium freischalten (Einmaliger Kauf) + + + Unbegrenzte Downloads & Ansichten vom Komga-Server + + + Freigeschaltet + + + Premium-Funktionen freischalten + + + Lizenz & Statistik + + + Gesperrt + + + Gelesene Seiten + + + Testversion der lokalen Bibliothek (Importe & Ansichten pro Format) + + + Premium-Edition + + + Lokale Importe + + + Kostenlose Version weiter testen + + + Führen Sie ein Upgrade auf Premium durch, um unbegrenzte Importe und Ansichten zu erhalten. + + + Sie haben einige Limits der Testversion in der kostenlosen Version erreicht. Um unbegrenzt Comics zu lesen, herunterzuladen und zu importieren, unterstützen Sie die App, indem Sie das Premium-Upgrade erwerben! + + + Testversion + + + Unterstützt die laufende Entwicklung & Updates + + + Ansichten + + + Komga-Downloads + + + Unbegrenzte lokale Bibliotheks-Importe + + + Limit für aktive Downloads & permanente Ansichten + + + Vielen Dank für Ihre Unterstützung von StripWolf! Alle Funktionen freigeschaltet. + + + Komga-Server-Integration + \ No newline at end of file diff --git a/src/StripWolf/StripWolf.Core/Resources/Strings.es.resx b/src/StripWolf/StripWolf.Core/Resources/Strings.es.resx index 6db10d0..f33a963 100644 --- a/src/StripWolf/StripWolf.Core/Resources/Strings.es.resx +++ b/src/StripWolf/StripWolf.Core/Resources/Strings.es.resx @@ -1,4 +1,4 @@ - + @@ -963,4 +963,79 @@ Finalizar + + Ha alcanzado algunos límites de la versión de prueba. Desbloquee premium para leer este cómic. + + + Panel de estadísticas de lectura personal + + + Cómics abiertos + + + Estadísticas de lectura personal + + + Desbloquear Premium (Compra única) + + + Descargas y visualizaciones ilimitadas del servidor Komga + + + Desbloqueado + + + Desbloquear funciones premium + + + Licencia y estadísticas + + + Bloqueado + + + Páginas leídas + + + Prueba de biblioteca local (Importaciones y vistas por formato) + + + Edición Premium + + + Importaciones locales + + + Seguir probando la versión gratuita + + + Actualice a Premium para obtener importaciones y visualizaciones ilimitadas. + + + Ha alcanzado algunos límites de la versión de prueba en la versión gratuita. Para leer, descargar e importar cómics ilimitados, ¡apoye la aplicación comprando el desbloqueo premium! + + + Edición de prueba + + + Apoya el desarrollo continuo y las actualizaciones + + + Vistas + + + Descargas de Komga + + + Importaciones locales ilimitadas a la biblioteca + + + Límite de descargas activas y vistas permanentes + + + ¡Gracias por apoyar a StripWolf! Todas las funciones desbloqueadas. + + + Integración con el servidor Komga + \ No newline at end of file diff --git a/src/StripWolf/StripWolf.Core/Resources/Strings.fr.resx b/src/StripWolf/StripWolf.Core/Resources/Strings.fr.resx index 92eca3b..a3b73ab 100644 --- a/src/StripWolf/StripWolf.Core/Resources/Strings.fr.resx +++ b/src/StripWolf/StripWolf.Core/Resources/Strings.fr.resx @@ -1,4 +1,4 @@ - + @@ -963,4 +963,79 @@ Terminer + + Vous avez atteint certaines limites de la version d'essai. Veuillez débloquer la version premium pour lire cette bande dessinée. + + + Tableau de bord des statistiques de lecture personnelles + + + Bandes dessinées ouvertes + + + Statistiques de lecture personnelles + + + Débloquer Premium (Achat unique) + + + Téléchargements et visionnages illimités sur le serveur Komga + + + Débloqué + + + Débloquer les fonctionnalités premium + + + Licence et statistiques + + + Verrouillé + + + Pages lues + + + Essai de bibliothèque locale (Imports et lectures par format) + + + Édition Premium + + + Importations locales + + + Continuer à tester la version gratuite + + + Passez à la version Premium pour obtenir des imports et des lectures illimités. + + + Vous avez atteint certaines limites de la version d'essai dans la version gratuite. Pour lire, télécharger et importer des bandes dessinées en illimité, soutenez l'application en achetant le déverrouillage premium ! + + + Édition d'essai + + + Soutient le développement et les mises à jour en cours + + + Visionnages + + + Téléchargements Komga + + + Imports de bibliothèque locale illimités + + + Limite des téléchargements actifs et des lectures permanentes + + + Merci de soutenir StripWolf ! Toutes les fonctionnalités sont débloquées. + + + Intégration du serveur Komga + \ No newline at end of file diff --git a/src/StripWolf/StripWolf.Core/Resources/Strings.nl.resx b/src/StripWolf/StripWolf.Core/Resources/Strings.nl.resx index 7923b5d..3d286b4 100644 --- a/src/StripWolf/StripWolf.Core/Resources/Strings.nl.resx +++ b/src/StripWolf/StripWolf.Core/Resources/Strings.nl.resx @@ -1,4 +1,4 @@ - + @@ -973,4 +973,79 @@ Voltooien + + Je hebt enkele limieten van de proefversie bereikt. Ontgrendel premium om deze strip te lezen. + + + Persoonlijk leesstatistieken dashboard + + + Geopende strips + + + Persoonlijke leesstatistieken + + + Premium ontgrendelen (Eenmalige aankoop) + + + Onbeperkt downloaden & bekijken vanaf de Komga-server + + + Ontgrendeld + + + Premium functies ontgrendelen + + + Licentie & statistieken + + + Vergrendeld + + + Gelezen pagina's + + + Lokale bibliotheek proefversie (Importen & weergaven per formaat) + + + Premium-editie + + + Lokale importen + + + De gratis versie blijven proberen + + + Upgrade naar Premium voor onbeperkt importeren en bekijken. + + + Je hebt enkele limieten van de proefversie in de gratis versie bereikt. Om onbeperkt strips te lezen, downloaden en importeren, kun je de app steunen door de premium ontgrendeling te kopen! + + + Proefversie + + + Ondersteunt voortdurende ontwikkeling & updates + + + Weergaven + + + Komga downloads + + + Onbeperkte lokale bibliotheekimporten + + + Limiet voor actieve downloads & permanente weergaven + + + Bedankt voor het steunen van StripWolf! Alle functies ontgrendeld. + + + Komga server integratie + \ No newline at end of file diff --git a/src/StripWolf/StripWolf.Core/Resources/Strings.resx b/src/StripWolf/StripWolf.Core/Resources/Strings.resx index 4a158a2..6692c0e 100644 --- a/src/StripWolf/StripWolf.Core/Resources/Strings.resx +++ b/src/StripWolf/StripWolf.Core/Resources/Strings.resx @@ -1,4 +1,4 @@ - + @@ -1075,4 +1075,79 @@ Finish + + You have reached some limits of the trial. Please unlock premium to read this comic. + + + Personal reading statistics dashboard + + + Comics Opened + + + Personal Reading Statistics + + + Unlock Premium (One-Time Purchase) + + + Unlimited Komga server downloads & viewing + + + Unlocked + + + Unlock Premium Features + + + Licence & Statistics + + + Locked + + + Pages Read + + + Local Library Trial (Imports & Views per format) + + + Premium Edition + + + Local Imports + + + Keep Testing Free Version + + + Upgrade to Premium to get unlimited imports and views. + + + You have reached some limits of the trial in the free version. To read, download, and import unlimited comics, support the app by purchasing the premium unlock! + + + Trial Edition + + + Supports ongoing development & updates + + + Views + + + Komga Downloads + + + Unlimited local library imports + + + Active downloads & permanent views limit + + + Thank you for supporting StripWolf! All features unlocked. + + + Komga Server Integration + \ No newline at end of file diff --git a/src/StripWolf/StripWolf.Core/Services/AppEventsService.cs b/src/StripWolf/StripWolf.Core/Services/AppEventsService.cs new file mode 100644 index 0000000..90181c8 --- /dev/null +++ b/src/StripWolf/StripWolf.Core/Services/AppEventsService.cs @@ -0,0 +1,35 @@ +using System; +using StripWolf.Core.Models; + +namespace StripWolf.Core.Services; + +/// +/// Centralized implementation of IAppEventsService to publish application-wide events. +/// +public class AppEventsService : IAppEventsService +{ + public event EventHandler? LocalComicImported; + public event EventHandler? KomgaBookDownloaded; + public event EventHandler? ComicOpened; + public event EventHandler? PageRead; + + public void RaiseLocalComicImported(string filePath) + { + LocalComicImported?.Invoke(this, filePath); + } + + public void RaiseKomgaBookDownloaded(string bookId) + { + KomgaBookDownloaded?.Invoke(this, bookId); + } + + public void RaiseComicOpened(int comicId, ComicSource source, string identifier) + { + ComicOpened?.Invoke(this, new ComicOpenedEventArgs(comicId, source, identifier)); + } + + public void RaisePageRead() + { + PageRead?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/StripWolf/StripWolf.Core/Services/IAppEventsService.cs b/src/StripWolf/StripWolf.Core/Services/IAppEventsService.cs new file mode 100644 index 0000000..eddd5bb --- /dev/null +++ b/src/StripWolf/StripWolf.Core/Services/IAppEventsService.cs @@ -0,0 +1,34 @@ +using System; +using StripWolf.Core.Models; + +namespace StripWolf.Core.Services; + +public class ComicOpenedEventArgs : EventArgs +{ + public int ComicId { get; } + public ComicSource Source { get; } + public string Identifier { get; } // FilePath for local, BookId for Komga + + public ComicOpenedEventArgs(int comicId, ComicSource source, string identifier) + { + ComicId = comicId; + Source = source; + Identifier = identifier; + } +} + +/// +/// Centralized service to publish and subscribe to application-wide events. +/// +public interface IAppEventsService +{ + event EventHandler? LocalComicImported; + event EventHandler? KomgaBookDownloaded; + event EventHandler? ComicOpened; + event EventHandler? PageRead; + + void RaiseLocalComicImported(string filePath); + void RaiseKomgaBookDownloaded(string bookId); + void RaiseComicOpened(int comicId, ComicSource source, string identifier); + void RaisePageRead(); +} diff --git a/src/StripWolf/StripWolf.Core/Services/IBillingService.cs b/src/StripWolf/StripWolf.Core/Services/IBillingService.cs new file mode 100644 index 0000000..ed2c871 --- /dev/null +++ b/src/StripWolf/StripWolf.Core/Services/IBillingService.cs @@ -0,0 +1,15 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace StripWolf.Core.Services; + +/// +/// Platform-specific billing interface to verify Google Play/iOS purchases. +/// +public interface IBillingService +{ + /// + /// Queries the platform's app store to check if the premium unlock product is owned. + /// + Task QueryPremiumPurchaseAsync(CancellationToken cancellationToken); +} diff --git a/src/StripWolf/StripWolf.Core/Services/LibraryService.cs b/src/StripWolf/StripWolf.Core/Services/LibraryService.cs index 3c7775b..723b4b1 100644 --- a/src/StripWolf/StripWolf.Core/Services/LibraryService.cs +++ b/src/StripWolf/StripWolf.Core/Services/LibraryService.cs @@ -60,6 +60,9 @@ public class LibraryService /// public event EventHandler? LibraryChanged; + private readonly TrialService _trialService; + private readonly IAppEventsService _appEventsService; + public LibraryService( DatabaseService databaseService, ComicReaderService comicReaderService, @@ -69,7 +72,9 @@ public LibraryService( PdfToCbzConverterService pdfConverter, EpubToCbzConverterService epubConverter, EpubShadowConversionService epubShadowConversionService, - ComicConverterService comicConverter) + ComicConverterService comicConverter, + TrialService trialService, + IAppEventsService appEventsService) { _databaseService = databaseService; _comicReaderService = comicReaderService; @@ -80,6 +85,8 @@ public LibraryService( _epubConverter = epubConverter; _epubShadowConversionService = epubShadowConversionService; _comicConverter = comicConverter; + _trialService = trialService; + _appEventsService = appEventsService; _appDataDirectory = GetAppDataDirectory(); _comicsDirectory = Path.Combine(_appDataDirectory, "Comics"); @@ -347,6 +354,13 @@ public async Task ImportLocalComicAsync(string filePath, IProgress ImportLocalComicAsync(string filePath, IProgress ImportLocalComicAsync(string filePath, IProgress ImportLocalComicAsync(string filePath, IProgress +/// Service to handle trial limitations and license unlock logic. +/// +public class TrialService +{ + public const int MaxTrialLimit = 5; + + public static readonly System.Collections.Generic.HashSet AllowedFormats = new(StringComparer.OrdinalIgnoreCase) + { + "cbz", "cbr", "cbt", "cb7", "epub", "pdf" + }; + + private readonly System.Threading.SemaphoreSlim _settingsSemaphore = new(1, 1); + + private readonly SettingsService _settingsService; + private readonly DatabaseService _databaseService; + + public event EventHandler? PremiumUnlockRequested; + + public void RequestPremiumUnlock() + { + PremiumUnlockRequested?.Invoke(this, EventArgs.Empty); + } + + public TrialService( + SettingsService settingsService, + DatabaseService databaseService, + IAppEventsService appEventsService, + IBillingService? billingService = null) + { + _settingsService = settingsService; + _databaseService = databaseService; + + appEventsService.LocalComicImported += OnLocalComicImported; + appEventsService.KomgaBookDownloaded += OnKomgaBookDownloaded; + appEventsService.ComicOpened += OnComicOpened; + appEventsService.PageRead += OnPageRead; + + if (billingService is not null) + { + InitializeBillingBackground(billingService); + } + } + + private void InitializeBillingBackground(IBillingService billingService) + { + _ = Task.Run(async () => + { + try + { + // Wait briefly during startup to avoid competing with UI render thread + await Task.Delay(2000); + + var hasPremium = await billingService.QueryPremiumPurchaseAsync(System.Threading.CancellationToken.None); + + if (hasPremium != IsUnlimitedUnlocked) + { + await _settingsService.UpdateSettingsAsync(s => + { + s.IsUnlimitedUnlocked = hasPremium; + }); + } + } + catch + { + // Fail silently (keep using cached value if offline or Play Store is unreachable) + } + }); + } + + private async void OnLocalComicImported(object? sender, string filePath) + { + await LogUsageAsync("LocalImport", filePath); + } + + private async void OnKomgaBookDownloaded(object? sender, string bookId) + { + await LogUsageAsync("KomgaDownload", bookId); + } + + private async void OnPageRead(object? sender, EventArgs e) + { + await LogUsageAsync("PagesRead"); + } + + private async void OnComicOpened(object? sender, ComicOpenedEventArgs e) + { + try + { + await LogUsageAsync("ComicOpen", e.ComicId); + + if (!IsUnlimitedUnlocked) + { + await _settingsSemaphore.WaitAsync(); + try + { + if (e.Source == ComicSource.Komga) + { + if (int.TryParse(e.Identifier, out var bookId)) + { + await _settingsService.UpdateSettingsAsync(s => + { + if (!s.PermanentViewedKomgaBookIds.Contains(bookId)) + { + s.PermanentViewedKomgaBookIds.Add(bookId); + } + }); + } + } + else + { + var filename = Path.GetFileName(e.Identifier); + await _settingsService.UpdateSettingsAsync(s => + { + if (!s.PermanentViewedLocalPaths.Contains(filename)) + { + s.PermanentViewedLocalPaths.Add(filename); + } + }); + } + } + finally + { + _settingsSemaphore.Release(); + } + } + } + catch + { + // Gracefully ignore settings update/I/O exceptions from async void event handler + } + } + + public bool IsUnlimitedUnlocked + { + get + { +#if PLAY_STORE_BUILD + return _settingsService.LoadSettings().IsUnlimitedUnlocked; +#else + return true; +#endif + } + } + + /// + /// Checks if a local import of a specific file type is allowed (capacity limit: max MaxTrialLimit). + /// + public async Task CanImportLocalAsync(string filePath) + { + if (IsUnlimitedUnlocked) return true; + + var ext = Path.GetExtension(filePath)?.TrimStart('.')?.ToLowerInvariant(); + if (string.IsNullOrEmpty(ext)) return true; + + if (!AllowedFormats.Contains(ext)) return true; + + // Count how many local comics of this extension currently exist in the database library + var comics = await _databaseService.GetComicsAsync(); + var currentCount = comics.Count(c => + { + if (c.Source == ComicSource.Komga) return false; + var cExt = Path.GetExtension(c.FilePath)?.TrimStart('.')?.ToLowerInvariant(); + return cExt == ext; + }); + + return currentCount < MaxTrialLimit; + } + + /// + /// Checks if a Komga download is allowed (capacity limit: max MaxTrialLimit). + /// + public async Task CanDownloadKomgaAsync() + { + if (IsUnlimitedUnlocked) return true; + + // Count how many Komga-sourced comics currently exist in the library + var comics = await _databaseService.GetComicsAsync(); + var currentCount = comics.Count(c => c.Source == ComicSource.Komga); + + return currentCount < MaxTrialLimit; + } + + /// + /// Checks if opening a local comic is allowed (permanent view limit: max MaxTrialLimit unique files per format). + /// + public async Task CanOpenLocalAsync(string filePath) + { + if (IsUnlimitedUnlocked) return true; + + var ext = Path.GetExtension(filePath)?.TrimStart('.')?.ToLowerInvariant(); + if (string.IsNullOrEmpty(ext)) return true; + + var filename = Path.GetFileName(filePath); + var settings = _settingsService.LoadSettings(); + + // Already viewed in trial - free to open again + if (settings.PermanentViewedLocalPaths.Contains(filename) || settings.PermanentViewedLocalPaths.Contains(filePath)) + { + return true; + } + + // Count how many unique viewed local files of this extension have been opened permanently + var currentViewedCount = settings.PermanentViewedLocalPaths + .Count(p => Path.GetExtension(p)?.TrimStart('.')?.ToLowerInvariant() == ext); + + return currentViewedCount < MaxTrialLimit; + } + + /// + /// Checks if opening a Komga comic is allowed (permanent view limit: max MaxTrialLimit unique files). + /// + public async Task CanOpenKomgaAsync(int bookId) + { + if (IsUnlimitedUnlocked) return true; + + var settings = _settingsService.LoadSettings(); + + // Already viewed in trial - free to open again + if (settings.PermanentViewedKomgaBookIds.Contains(bookId)) + { + return true; + } + + return settings.PermanentViewedKomgaBookIds.Count < MaxTrialLimit; + } + + /// + /// Simulates/performs premium unlock. + /// + public async Task UnlockPremiumAsync() + { + await _settingsService.UpdateSettingsAsync(s => + { + s.IsUnlimitedUnlocked = true; + }); + } + + /// + /// Logs usage statistics to the database. + /// + public async Task LogUsageAsync(string metric, object? metadata = null) + { + await _databaseService.LogUsageAsync(metric, metadata?.ToString()); + } +} diff --git a/src/StripWolf/StripWolf.Core/StripWolf.Core.csproj b/src/StripWolf/StripWolf.Core/StripWolf.Core.csproj index 97e3b83..78d8baa 100644 --- a/src/StripWolf/StripWolf.Core/StripWolf.Core.csproj +++ b/src/StripWolf/StripWolf.Core/StripWolf.Core.csproj @@ -1,4 +1,4 @@ - + net10.0 enable @@ -15,6 +15,7 @@ true true StripWolf.Core + true @@ -25,6 +26,10 @@ $(DefineConstants);DISABLE_GUIDED_READING + + $(DefineConstants);PLAY_STORE_BUILD + + diff --git a/src/StripWolf/StripWolf.Core/ViewModels/MainViewModel.cs b/src/StripWolf/StripWolf.Core/ViewModels/MainViewModel.cs index 6280020..26cd6af 100644 --- a/src/StripWolf/StripWolf.Core/ViewModels/MainViewModel.cs +++ b/src/StripWolf/StripWolf.Core/ViewModels/MainViewModel.cs @@ -35,7 +35,11 @@ public record WelcomeThemeOption(AppThemePreference Value, string DisplayName); /// public partial class MainViewModel : ViewModelBase { +#if PLAY_STORE_BUILD + private const int WelcomeExperienceStepCount = 6; +#else private const int WelcomeExperienceStepCount = 7; +#endif private readonly LibraryViewModel _libraryViewModel; private readonly KomgaViewModel _komgaViewModel; private readonly ActivityViewModel _activityViewModel; @@ -46,6 +50,7 @@ public partial class MainViewModel : ViewModelBase private readonly IExternalLinkService _externalLinkService; private readonly KomgaSyncService _komgaSyncService; private readonly UpdateService _updateService; + private readonly TrialService _trialService; private bool _isInitializing = true; private bool _isApplyingWelcomePreferences; private bool _shouldStartWelcomeAfterInitialization; @@ -76,6 +81,9 @@ public partial class MainViewModel : ViewModelBase [ObservableProperty] private bool _showWelcomeExperience; + [ObservableProperty] + private bool _showPremiumUnlockDialog; + [ObservableProperty] private int _welcomeExperienceStep; @@ -101,7 +109,8 @@ public MainViewModel( LocalizationService localizationService, IExternalLinkService externalLinkService, KomgaSyncService komgaSyncService, - UpdateService updateService) + UpdateService updateService, + TrialService trialService) { _libraryViewModel = libraryViewModel; _komgaViewModel = komgaViewModel; @@ -113,6 +122,9 @@ public MainViewModel( _externalLinkService = externalLinkService; _komgaSyncService = komgaSyncService; _updateService = updateService; + _trialService = trialService; + + _trialService.PremiumUnlockRequested += (s, e) => ShowPremiumUnlockDialog = true; Title = "StripWolf"; _currentView = _libraryViewModel; @@ -344,8 +356,13 @@ private void OnReaderCloseRequested(object? sender, EventArgs e) public bool IsWelcomeLibraryImportStep => ShowWelcomeExperience && WelcomeExperienceStep == 2; public bool IsWelcomeKomgaStep => ShowWelcomeExperience && WelcomeExperienceStep == 3; public bool IsWelcomeActivityStep => ShowWelcomeExperience && WelcomeExperienceStep == 4; +#if PLAY_STORE_BUILD + public bool IsWelcomeSettingsStep => ShowWelcomeExperience && WelcomeExperienceStep == 5; + public bool IsWelcomeSupportStep => false; +#else public bool IsWelcomeSettingsStep => ShowWelcomeExperience && (WelcomeExperienceStep == 5 || WelcomeExperienceStep == 6); public bool IsWelcomeSupportStep => ShowWelcomeExperience && WelcomeExperienceStep == 6; +#endif public bool HasWelcomeExperienceTargetHint => !string.IsNullOrWhiteSpace(WelcomeExperienceTargetHint); public double WelcomeBackgroundContentOpacity => ShowWelcomeExperience && WelcomeExperienceStep == 0 ? 0.08 : 1.0; public string WelcomeExperienceStepTitle => WelcomeExperienceStep switch @@ -356,7 +373,9 @@ private void OnReaderCloseRequested(object? sender, EventArgs e) 3 => Resources.Loc.Instance.WelcomeExperienceKomgaTitle, 4 => Resources.Loc.Instance.WelcomeExperienceActivityTitle, 5 => Resources.Loc.Instance.WelcomeExperienceSettingsTitle, +#if !PLAY_STORE_BUILD 6 => Resources.Loc.Instance.WelcomeExperienceSupportTitle, +#endif _ => Resources.Loc.Instance.WelcomeExperienceIntroTitle }; public string WelcomeExperienceStepDescription => WelcomeExperienceStep switch @@ -367,7 +386,9 @@ private void OnReaderCloseRequested(object? sender, EventArgs e) 3 => Resources.Loc.Instance.WelcomeExperienceKomgaDescription, 4 => Resources.Loc.Instance.WelcomeExperienceActivityDescription, 5 => Resources.Loc.Instance.WelcomeExperienceSettingsDescription, +#if !PLAY_STORE_BUILD 6 => Resources.Loc.Instance.WelcomeExperienceSupportDescription, +#endif _ => Resources.Loc.Instance.WelcomeExperienceIntroDescription }; public string WelcomeExperienceTargetHint => WelcomeExperienceStep switch @@ -377,7 +398,9 @@ private void OnReaderCloseRequested(object? sender, EventArgs e) 3 => Resources.Loc.Instance.WelcomeExperienceKomgaHint, 4 => Resources.Loc.Instance.WelcomeExperienceActivityHint, 5 => Resources.Loc.Instance.WelcomeExperienceSettingsHint, +#if !PLAY_STORE_BUILD 6 => Resources.Loc.Instance.WelcomeExperienceSupportHint, +#endif _ => string.Empty }; @@ -545,7 +568,9 @@ private void SetWelcomeExperienceStep(int step) 3 => 1, 4 => 2, 5 => 3, +#if !PLAY_STORE_BUILD 6 => 3, +#endif _ => (int?)null }; @@ -670,4 +695,17 @@ private static void ApplyTheme(AppThemePreference theme) _ => ThemeVariant.Default }; } + + [RelayCommand] + private void ClosePremiumUnlockDialog() + { + ShowPremiumUnlockDialog = false; + } + + [RelayCommand] + private async Task PurchasePremiumUnlock() + { + await _trialService.UnlockPremiumAsync(); + ShowPremiumUnlockDialog = false; + } } diff --git a/src/StripWolf/StripWolf.Core/ViewModels/ReaderViewModel.cs b/src/StripWolf/StripWolf.Core/ViewModels/ReaderViewModel.cs index 5450480..099de20 100644 --- a/src/StripWolf/StripWolf.Core/ViewModels/ReaderViewModel.cs +++ b/src/StripWolf/StripWolf.Core/ViewModels/ReaderViewModel.cs @@ -629,6 +629,9 @@ private async Task ResolveAutomaticReadingDirectionAsync() /// public event EventHandler? CloseRequested; + private readonly TrialService _trialService; + private readonly IAppEventsService _appEventsService; + public ReaderViewModel( LibraryService libraryService, ComicReaderService comicReaderService, @@ -636,7 +639,9 @@ public ReaderViewModel( KomgaApiServiceFactory komgaApiServiceFactory, PanelDetectionService panelDetectionService, SettingsService settingsService, - EpubShadowConversionService epubShadowConversionService) + EpubShadowConversionService epubShadowConversionService, + TrialService trialService, + IAppEventsService appEventsService) { _libraryService = libraryService; _comicReaderService = comicReaderService; @@ -645,6 +650,8 @@ public ReaderViewModel( _panelDetectionService = panelDetectionService; _settingsService = settingsService; _epubShadowConversionService = epubShadowConversionService; + _trialService = trialService; + _appEventsService = appEventsService; _epubShadowConversionService.ConversionStateChanged += OnEpubConversionStateChanged; _periodicSyncTimer = new DispatcherTimer @@ -879,11 +886,39 @@ public async Task LoadComicAsync(int comicId) Comic = await _libraryService.GetComicAsync(ComicId); if (Comic is not null) { + // Enforce permanent view limits in trial + bool allowed = true; + if (Comic.Source == ComicSource.Komga && !string.IsNullOrEmpty(Comic.KomgaId)) + { + if (int.TryParse(Comic.KomgaId, out var komgaBookId)) + { + allowed = await _trialService.CanOpenKomgaAsync(komgaBookId); + } + } + else + { + allowed = await _trialService.CanOpenLocalAsync(Comic.FilePath); + } + + if (!allowed) + { + IsBusy = false; + ErrorMessage = Resources.Loc.Instance.TrialLimitReachedError; + _trialService.RequestPremiumUnlock(); + // Navigate back or close the reader since opening is blocked + Dispatcher.UIThread.Post(() => _ = GoBackAsync()); + return; + } + // Save last opened comic info in settings settings.LastOpenedComicId = Comic.Id; settings.LastOpenedComicPath = Comic.FilePath; settings.WasInReader = true; _ = _settingsService.SaveSettingsAsync(settings); + _appEventsService.RaiseComicOpened( + Comic.Id, + Comic.Source, + Comic.Source == ComicSource.Komga ? (Comic.KomgaId ?? string.Empty) : Comic.FilePath); Title = Comic.Title; OnPropertyChanged(nameof(MaxSliderValue)); @@ -1158,6 +1193,7 @@ private async Task LoadPageAsync() } _lastLoadedPageIndex = pageIndex; + _appEventsService.RaisePageRead(); // Reset zoom when changing pages ZoomLevel = 1.0; diff --git a/src/StripWolf/StripWolf.Core/ViewModels/SettingsViewModel.cs b/src/StripWolf/StripWolf.Core/ViewModels/SettingsViewModel.cs index d372804..9edcb6f 100644 --- a/src/StripWolf/StripWolf.Core/ViewModels/SettingsViewModel.cs +++ b/src/StripWolf/StripWolf.Core/ViewModels/SettingsViewModel.cs @@ -17,13 +17,17 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; +using System.IO; +using System.Linq; using Avalonia; using Avalonia.Threading; using Avalonia.Styling; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using StripWolf.Core.Data; using StripWolf.Core.Models; using StripWolf.Core.Resources; using StripWolf.Core.Services; @@ -49,6 +53,8 @@ public partial class SettingsViewModel : ViewModelBase private readonly LocalizationService _localizationService; private readonly IExternalLinkService _externalLinkService; private readonly UpdateService _updateService; + private readonly TrialService _trialService; + private readonly DatabaseService _databaseService; private AppSettings? _appSettings; private int _nextServerId = 1; @@ -108,6 +114,13 @@ public partial class SettingsViewModel : ViewModelBase [ObservableProperty] private bool _isSupportMeVisible; + public bool IsSupportMeAllowed => +#if PLAY_STORE_BUILD + false; +#else + true; +#endif + public string AppVersion { get @@ -313,13 +326,66 @@ public string AppVersion public UpdateService UpdateService => _updateService; - public SettingsViewModel(SettingsService settingsService, LibraryService libraryService, LocalizationService localizationService, IExternalLinkService externalLinkService, UpdateService updateService) + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsPlayStoreBuildAndUnlocked))] + [NotifyPropertyChangedFor(nameof(IsPlayStoreBuildAndLocked))] + private bool _isUnlimitedUnlocked; + + public bool IsPlayStoreBuildAndUnlocked => IsPlayStoreBuild && IsUnlimitedUnlocked; + + public bool IsPlayStoreBuildAndLocked => IsPlayStoreBuild && !IsUnlimitedUnlocked; + + public string LicenceSectionHeader => IsPlayStoreBuild ? Resources.Loc.Instance.TrialLicenceAndStats : Resources.Loc.Instance.TrialPersonalReadingStats; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(KomgaDownloadsDisplay))] + private int _komgaDownloadsUsed; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(KomgaViewsDisplay))] + private int _komgaViewsUsed; + + public string KomgaDownloadsDisplay => $"{KomgaDownloadsUsed}/{TrialService.MaxTrialLimit}"; + public string KomgaViewsDisplay => $"{KomgaViewsUsed}/{TrialService.MaxTrialLimit}"; + + [ObservableProperty] + private ObservableCollection _localFormatStatuses = []; + + [ObservableProperty] + private int _totalPagesRead; + + [ObservableProperty] + private int _totalComicsOpened; + + [ObservableProperty] + private int _totalLocalImports; + + [ObservableProperty] + private int _totalKomgaDownloads; + + public bool IsPlayStoreBuild => +#if PLAY_STORE_BUILD + true; +#else + false; +#endif + + public SettingsViewModel( + SettingsService settingsService, + LibraryService libraryService, + LocalizationService localizationService, + IExternalLinkService externalLinkService, + UpdateService updateService, + TrialService trialService, + DatabaseService databaseService) { _settingsService = settingsService; _libraryService = libraryService; _localizationService = localizationService; _externalLinkService = externalLinkService; _updateService = updateService; + _trialService = trialService; + _databaseService = databaseService; Title = "Settings"; _settingsService.SettingsChanged += (_, settings) => { @@ -329,6 +395,7 @@ public SettingsViewModel(SettingsService settingsService, LibraryService library ReplaceSectionCollection(LibrarySections, settings.LibrarySections); ReplaceSectionCollection(KomgaSections, settings.KomgaSections); OnPropertyChanged(nameof(ActiveServerId)); + _ = LoadTrialStatusAndStatsAsync(); }); }; } @@ -541,6 +608,7 @@ private void LoadServers() ReplaceSectionCollection(LibrarySections, _appSettings.LibrarySections); ReplaceSectionCollection(KomgaSections, _appSettings.KomgaSections); + _ = LoadTrialStatusAndStatsAsync(); } partial void OnSyncReadProgressChanged(bool value) @@ -1157,4 +1225,102 @@ targetCollection is null || await PersistSectionLayoutAsync(section); } + + [RelayCommand] + private void RequestUnlock() + { + _trialService.RequestPremiumUnlock(); + } + + public async Task LoadTrialStatusAndStatsAsync() + { + IsUnlimitedUnlocked = _trialService.IsUnlimitedUnlocked; + + // Load stats from database + TotalPagesRead = await _databaseService.GetUsageCountAsync("PagesRead"); + TotalComicsOpened = await _databaseService.GetUsageCountAsync("ComicOpen"); + TotalLocalImports = await _databaseService.GetUsageCountAsync("LocalImport"); + TotalKomgaDownloads = await _databaseService.GetUsageCountAsync("KomgaDownload"); + + if (!IsUnlimitedUnlocked) + { + // Load trial limits + var settings = _settingsService.LoadSettings(); + var comics = await _databaseService.GetComicsAsync(); + + KomgaDownloadsUsed = comics.Count(c => c.Source == ComicSource.Komga); + KomgaViewsUsed = settings.PermanentViewedKomgaBookIds.Count; + + var formats = TrialService.AllowedFormats; + var formatList = new List(); + + foreach (var fmt in formats) + { + var imports = comics.Count(c => + { + if (c.Source == ComicSource.Komga) return false; + var cExt = Path.GetExtension(c.FilePath)?.TrimStart('.')?.ToLowerInvariant(); + return cExt == fmt; + }); + + var views = settings.PermanentViewedLocalPaths.Count(p => + { + var cExt = Path.GetExtension(p)?.TrimStart('.')?.ToLowerInvariant(); + return cExt == fmt; + }); + + formatList.Add(new FormatTrialStatus(fmt.ToUpperInvariant(), imports, views)); + } + + Dispatcher.UIThread.Post(() => + { + LocalFormatStatuses.Clear(); + foreach (var status in formatList) + { + LocalFormatStatuses.Add(status); + } + }); + } + } + + [RelayCommand] + private void ReportIssue() + { + string platform = "Unknown"; + if (System.OperatingSystem.IsAndroid()) platform = "Android"; + else if (System.OperatingSystem.IsWindows()) platform = "Windows"; + else if (System.OperatingSystem.IsMacOS()) platform = "macOS"; + else if (System.OperatingSystem.IsLinux()) platform = "Linux"; + else if (System.OperatingSystem.IsIOS()) platform = "iOS"; + + var osDesc = System.Runtime.InteropServices.RuntimeInformation.OSDescription; + var version = AppVersion; + + var body = $""" +**Describe the bug** +[A clear and concise description of what the bug is.] + +**To Reproduce** +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +**Environment Details:** +- App Version: {version} +- Platform: {platform} +- OS Description: {osDesc} +"""; + + var url = $"https://github.com/dapplo/StripWolf/issues/new?body={System.Uri.EscapeDataString(body)}"; + _externalLinkService.OpenUrl(url); + } +} + +public record FormatTrialStatus(string Format, int ImportsUsed, int ViewsUsed) +{ + public int ImportsRemaining => Math.Max(0, TrialService.MaxTrialLimit - ImportsUsed); + public int ViewsRemaining => Math.Max(0, TrialService.MaxTrialLimit - ViewsUsed); + public string ImportsDisplay => $"{ImportsUsed}/{TrialService.MaxTrialLimit}"; + public string ViewsDisplay => $"{ViewsUsed}/{TrialService.MaxTrialLimit}"; } diff --git a/src/StripWolf/StripWolf.Core/Views/MainView.axaml b/src/StripWolf/StripWolf.Core/Views/MainView.axaml index 2764978..283c0a7 100644 --- a/src/StripWolf/StripWolf.Core/Views/MainView.axaml +++ b/src/StripWolf/StripWolf.Core/Views/MainView.axaml @@ -252,5 +252,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +