Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand All @@ -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 \
Expand All @@ -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
30 changes: 30 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application android:label="StripWolf" android:icon="@drawable/icon"
android:networkSecurityConfig="@xml/network_security_config" />
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="true"
android:fullBackupContent="@xml/full_backup_content"
android:dataExtractionRules="@xml/backup_rules" />
</manifest>
14 changes: 14 additions & 0 deletions src/StripWolf/StripWolf.Android/Resources/xml/backup_rules.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<data-extraction-rules>
<cloud-backup>
<!-- Only backup settings and server credentials. Exclude DB and Covers. -->
<include domain="file" path=".local/share/StripWolf/settings.json" />
<include domain="file" path=".local/share/StripWolf/credentials.dat" />
<include domain="file" path=".local/share/StripWolf/.key" />
Comment thread
Lakritzator marked this conversation as resolved.
</cloud-backup>
<device-to-device-backup>
<include domain="file" path=".local/share/StripWolf/settings.json" />
<include domain="file" path=".local/share/StripWolf/credentials.dat" />
<include domain="file" path=".local/share/StripWolf/.key" />
</device-to-device-backup>
</data-extraction-rules>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<!-- Only backup settings and server credentials. Exclude DB and Covers. -->
<include domain="file" path=".local/share/StripWolf/settings.json" />
<include domain="file" path=".local/share/StripWolf/credentials.dat" />
<include domain="file" path=".local/share/StripWolf/.key" />
</full-backup-content>
13 changes: 13 additions & 0 deletions src/StripWolf/StripWolf.Core/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ public partial class App : Application
/// </summary>
public static Action<IServiceCollection>? RegisterFullScreenService { get; set; }

/// <summary>
/// Action to register the platform-specific billing service.
/// Set this before Initialize() is called if you need a custom implementation (e.g., on Android).
/// </summary>
public static Action<IServiceCollection>? RegisterBillingService { get; set; }

public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
Expand Down Expand Up @@ -183,6 +189,13 @@ private static void ConfigureServices(IServiceCollection services)
services.AddSingleton<ComicConverterService>();
services.AddSingleton<IExternalLinkService, ExternalLinkService>();
services.AddSingleton<UpdateService>();
services.AddSingleton<IAppEventsService, AppEventsService>();
services.AddSingleton<TrialService>();
Comment thread
Lakritzator marked this conversation as resolved.

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
Expand Down
40 changes: 40 additions & 0 deletions src/StripWolf/StripWolf.Core/Data/DatabaseService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SQLiteAsyncConnection> GetDatabaseAsync()
{
if (_isInitialized && _database is not null)
Expand Down Expand Up @@ -89,6 +92,7 @@ private async Task<SQLiteAsyncConnection> GetDatabaseAsync()
await _database.CreateTableAsync<KomgaServer>();
await _database.CreateTableAsync<KomgaPendingDownload>();
await _database.CreateTableAsync<KomgaPendingReadProgress>();
await _database.CreateTableAsync<UsageStats>();
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)");

Expand Down Expand Up @@ -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<int> GetUsageCountAsync(string metric)
{
try
{
var db = await GetDatabaseAsync();
return await db.Table<UsageStats>().Where(s => s.Metric == metric).CountAsync();
}
catch
{
return 0;
}
}

#endregion

public async ValueTask DisposeAsync()
{
if (_database is not null)
Expand Down
18 changes: 18 additions & 0 deletions src/StripWolf/StripWolf.Core/Models/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ public class AppSettings
/// </summary>
public bool? HasCompletedWelcomeExperience { get; set; }

/// <summary>
/// Whether the user has purchased the unlimited premium unlock.
/// </summary>
public bool IsUnlimitedUnlocked { get; set; } = false;

/// <summary>
/// Book IDs of Komga comics that have been opened (Max 2 in trial).
/// </summary>
public List<int> PermanentViewedKomgaBookIds { get; set; } = [];

/// <summary>
/// Local comic paths/filenames that have been opened (Max 2 per format in trial).
/// </summary>
public List<string> PermanentViewedLocalPaths { get; set; } = [];

/// <summary>
/// Preferred reading mode for the comic reader
/// </summary>
Expand Down Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions src/StripWolf/StripWolf.Core/Models/UsageStats.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using SQLite;

namespace StripWolf.Core.Models;

/// <summary>
/// Model for logging usage statistics in the database.
/// </summary>
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; }
}
27 changes: 27 additions & 0 deletions src/StripWolf/StripWolf.Core/Resources/Loc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Loading