Initial commit

This commit is contained in:
St. Nebula
2026-04-23 23:58:59 -05:00
commit 47b9e3c159
257 changed files with 18913 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:onsolgo/core/constants.dart';
class AchievementManager {
static Future<void> unlock(String uid, String achId) async {
final ref = FirebaseFirestore.instance.collection('users').doc(uid).collection('achievements').doc(achId);
final doc = await ref.get();
if (!doc.exists) {
await ref.set({'unlockedAt': FieldValue.serverTimestamp()});
}
}
static Future<void> incrementStat(String uid, String field, {int amount = 1}) async {
final userRef = FirebaseFirestore.instance.collection('users').doc(uid);
await userRef.update({field: FieldValue.increment(amount)});
final snap = await userRef.get();
final d = snap.data() as Map<String, dynamic>;
// Threshold Checks
if (field == 'pagesRead') {
int p = safeInt(d['pagesRead']);
if (p >= 10) unlock(uid, "page_turner_1");
if (p >= 50) unlock(uid, "page_turner_2");
if (p >= 200) unlock(uid, "page_turner_3");
}
if (field == 'chaptersRead') {
int c = safeInt(d['chaptersRead']);
if (c >= 1) unlock(uid, "chapter_complete_1");
if (c >= 10) unlock(uid, "chapter_complete_2");
if (c >= 50) unlock(uid, "chapter_complete_3");
}
}
}
+42
View File
@@ -0,0 +1,42 @@
import 'package:google_mobile_ads/google_mobile_ads.dart';
import 'package:flutter/foundation.dart';
import 'package:onsolgo/core/constants.dart';
class AdHandler {
static InterstitialAd? _interstitialAd;
static void loadInterstitialAd() {
if (kIsWeb) return; // Skip on PWA
InterstitialAd.load(
adUnitId: kInterstitialAdUnitId,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) => _interstitialAd = ad,
onAdFailedToLoad: (error) => _interstitialAd = null,
),
);
}
static void showInterstitialAd(Function onAdClosed) {
if (kIsWeb || _interstitialAd == null) {
onAdClosed(); // If web or ad not ready, just open the chapter
return;
}
_interstitialAd!.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
ad.dispose();
loadInterstitialAd(); // Load next one
onAdClosed(); // Open the chapter
},
onAdFailedToShowFullScreenContent: (ad, error) {
ad.dispose();
onAdClosed();
},
);
_interstitialAd!.show();
_interstitialAd = null;
}
}
+165
View File
@@ -0,0 +1,165 @@
import 'package:flutter/material.dart';
// --- BRANDING ---
const String kOnsolBanner = "https://files.onsolgo.cloud/manaa/assets/onsol-banner.png";
const String kOnsolAppWebUrl = 'https://onsol-go.web.app';
const Color kOnsolGold = Color(0xFFFFD700);
/// Share targets for web (query params; app may resolve these later).
String kOnsolSharePostUrl(String postId) => '$kOnsolAppWebUrl?post=$postId';
String kOnsolShareChapterUrl(String mangaId, Object? chapterNumber) =>
'$kOnsolAppWebUrl?manga=$mangaId&ch=$chapterNumber';
// --- THE 5-TIER RANK LOGIC ---
String getRankName(int level) {
switch (level) {
case 1: return "READER";
case 2: return "BACKER";
case 3: return "INVESTOR";
case 4: return "STEWARD";
case 5: return "VERIFIED ManaA ARTIST";
default: return "READER";
}
}
Color getRankColor(int level) {
switch (level) {
case 1: return Colors.grey;
case 2: return Colors.orangeAccent;
case 3: return Colors.cyanAccent;
case 4: return Colors.amber;
case 5: return kOnsolGold;
default: return Colors.grey;
}
}
IconData getRankIcon(int level) {
switch (level) {
case 1: return Icons.person;
case 2: return Icons.bolt;
case 3: return Icons.token;
case 4: return Icons.shield;
case 5: return Icons.verified;
default: return Icons.person;
}
}
Widget verifiedBadge(bool isVerified, {double size = 16}) {
if (!isVerified) return const SizedBox.shrink();
return Padding(
padding: const EdgeInsets.only(left: 6),
child: Icon(Icons.verified, color: kOnsolGold, size: size),
);
}
int safeInt(dynamic value) {
if (value == null) return 0;
if (value is int) return value;
if (value is double) return value.toInt();
if (value is String) return int.tryParse(value) ?? 0;
return 0;
}
// --- MASTER ACHIEVEMENT MODEL ---
class AchievementModel {
final String id, title, desc, category;
final IconData icon;
const AchievementModel({
required this.id,
required this.title,
required this.desc,
required this.category,
required this.icon
});
}
// --- ADMOB CONFIGURATION ---
// 1. THIS GOES IN YOUR ANDROID MANIFEST (Real App ID from AdMob)
const String kAdMobAppId = "ca-app-pub-7461835411004007~7412162757";
// 2. THE AD UNIT IDS
// For now, these are GOOGLE TEST IDs so you don't get banned.
// Replace these with your REAL IDs only when you upload to the Play Store.
// Unit: Interstitial (Full Screen)
const String kInterstitialAdUnitId = "ca-app-pub-3940256099942544/1033173712";
// Unit: Banner (Small strip)
const String kBannerAdUnitId = "ca-app-pub-3940256099942544/6300978111";
// --- FULL ACHIEVEMENT LIST (FROM PHOTOS) ---
const List<AchievementModel> kAllAchievements = [
// ACCOUNT & ONBOARDING
AchievementModel(id: "initiate", title: "INITIATE", desc: "Account created", category: "ACCOUNT", icon: Icons.flag),
AchievementModel(id: "profile_activated", title: "PROFILE ACTIVATED", desc: "Profile completed (bio + avatar)", category: "ACCOUNT", icon: Icons.face),
AchievementModel(id: "first_login", title: "FIRST LOGIN", desc: "First successful login session", category: "ACCOUNT", icon: Icons.login),
AchievementModel(id: "return_user", title: "RETURN USER", desc: "Logged in on 2 separate days", category: "ACCOUNT", icon: Icons.replay),
AchievementModel(id: "verified_access", title: "VERIFIED ACCESS", desc: "Email or phone verified", category: "ACCOUNT", icon: Icons.verified_user),
// READING PROGRESSION
AchievementModel(id: "first_contact", title: "FIRST CONTACT", desc: "Open first chapter", category: "READING", icon: Icons.play_arrow),
AchievementModel(id: "page_turner_1", title: "PAGE TURNER I", desc: "Read 10 pages", category: "READING", icon: Icons.auto_stories),
AchievementModel(id: "page_turner_2", title: "PAGE TURNER II", desc: "Read 50 pages", category: "READING", icon: Icons.menu_book),
AchievementModel(id: "page_turner_3", title: "PAGE TURNER III", desc: "Read 200 pages", category: "READING", icon: Icons.library_books),
AchievementModel(id: "chapter_complete_1", title: "CHAPTER COMPLETE I", desc: "Finish 1 chapter", category: "READING", icon: Icons.task_alt),
AchievementModel(id: "chapter_complete_2", title: "CHAPTER COMPLETE II", desc: "Finish 10 chapters", category: "READING", icon: Icons.done_all),
AchievementModel(id: "chapter_complete_3", title: "CHAPTER COMPLETE III", desc: "Finish 50 chapters", category: "READING", icon: Icons.checklist_rtl),
AchievementModel(id: "volume_devourer", title: "VOLUME DEVOURER", desc: "Finish a full volume", category: "READING", icon: Icons.auto_awesome_motion),
AchievementModel(id: "binge_reader", title: "BINGE READER", desc: "Read 3 chapters in one session", category: "READING", icon: Icons.flash_on),
AchievementModel(id: "locked_in", title: "LOCKED IN", desc: "Read 5 chapters in one session", category: "READING", icon: Icons.lock),
// COMMUNITY & ENGAGEMENT
AchievementModel(id: "voice_activated", title: "VOICE ACTIVATED", desc: "First comment posted", category: "COMMUNITY", icon: Icons.comment),
AchievementModel(id: "panel_reactor", title: "PANEL REACTOR", desc: "React to a panel", category: "COMMUNITY", icon: Icons.add_reaction),
AchievementModel(id: "conversation_starter", title: "CONV. STARTER", desc: "Start a thread", category: "COMMUNITY", icon: Icons.forum),
AchievementModel(id: "community_member", title: "COMMUNITY MEMBER", desc: "5 comments posted", category: "COMMUNITY", icon: Icons.groups),
AchievementModel(id: "recognized_voice", title: "RECOGNIZED VOICE", desc: "10 likes on a comment", category: "COMMUNITY", icon: Icons.campaign),
AchievementModel(id: "fan_favorite", title: "FAN FAVORITE", desc: "50 likes on a comment", category: "COMMUNITY", icon: Icons.favorite),
AchievementModel(id: "creator_noticed", title: "CREATOR NOTICED", desc: "Creator replies to you", category: "COMMUNITY", icon: Icons.star),
AchievementModel(id: "debate_ready", title: "DEBATE READY", desc: "Participate in 5 discussions", category: "COMMUNITY", icon: Icons.psychology),
// MARKETPLACE & COLLECTION
AchievementModel(id: "first_acquisition", title: "FIRST ACQUISITION", desc: "First purchase", category: "MARKET", icon: Icons.shopping_bag),
AchievementModel(id: "collector_1", title: "COLLECTOR I", desc: "Own 3 items", category: "MARKET", icon: Icons.layers),
AchievementModel(id: "collector_2", title: "COLLECTOR II", desc: "Own 10 items", category: "MARKET", icon: Icons.inventory_2),
AchievementModel(id: "archivist", title: "ARCHIVIST", desc: "Own a full volume", category: "MARKET", icon: Icons.folder_special),
AchievementModel(id: "limited_hunter", title: "LIMITED HUNTER", desc: "Purchase a limited drop", category: "MARKET", icon: Icons.timer),
AchievementModel(id: "artifact_holder", title: "ARTIFACT HOLDER", desc: "Purchase premium tier item", category: "MARKET", icon: Icons.token),
AchievementModel(id: "vault_builder", title: "VAULT BUILDER", desc: "Own items from 3 creators", category: "MARKET", icon: Icons.account_balance),
// SUPPORT & INVESTMENT
AchievementModel(id: "first_backer", title: "FIRST BACKER", desc: "First financial support", category: "INVEST", icon: Icons.volunteer_activism),
AchievementModel(id: "supporter", title: "SUPPORTER", desc: "Subscribe to a creator", category: "INVEST", icon: Icons.card_membership),
AchievementModel(id: "series_builder", title: "SERIES BUILDER", desc: "Support 3 creators", category: "INVEST", icon: Icons.build),
AchievementModel(id: "investor_1", title: "INVESTOR I", desc: "Spend \$25 total", category: "INVEST", icon: Icons.monetization_on),
AchievementModel(id: "investor_2", title: "INVESTOR II", desc: "Spend \$100 total", category: "INVEST", icon: Icons.payments),
AchievementModel(id: "investor_3", title: "INVESTOR III", desc: "Spend \$250 total", category: "INVEST", icon: Icons.account_balance_wallet),
AchievementModel(id: "day_one", title: "DAY ONE BACKER", desc: "Support series before Chapter 3", category: "INVEST", icon: Icons.first_page),
AchievementModel(id: "loyal_patron", title: "LOYAL PATRON", desc: "Maintain subscription for 3 months", category: "INVEST", icon: Icons.loyalty),
// CONSISTENCY
AchievementModel(id: "back_tomorrow", title: "BACK TOMORROW", desc: "2-day streak", category: "CONSISTENCY", icon: Icons.history),
AchievementModel(id: "discipline", title: "3-DAY DISCIPLINE", desc: "3-day streak", category: "CONSISTENCY", icon: Icons.self_improvement),
AchievementModel(id: "weekly_ritual", title: "WEEKLY RITUAL", desc: "7-day streak", category: "CONSISTENCY", icon: Icons.calendar_month),
AchievementModel(id: "monthly_reader", title: "MONTHLY READER", desc: "30-day streak", category: "CONSISTENCY", icon: Icons.calendar_today),
AchievementModel(id: "no_days_off", title: "NO DAYS OFF", desc: "60-day streak", category: "CONSISTENCY", icon: Icons.hotel_class),
// DISCOVERY
AchievementModel(id: "explorer_1", title: "EXPLORER I", desc: "Read 3 different series", category: "DISCOVERY", icon: Icons.explore),
AchievementModel(id: "explorer_2", title: "EXPLORER II", desc: "Read 5 different series", category: "DISCOVERY", icon: Icons.explore_off),
AchievementModel(id: "explorer_3", title: "EXPLORER III", desc: "Read 10 different series", category: "DISCOVERY", icon: Icons.public),
AchievementModel(id: "genre_breaker", title: "GENRE BREAKER", desc: "Read outside your usual category", category: "DISCOVERY", icon: Icons.extension),
AchievementModel(id: "new_blood", title: "NEW BLOOD", desc: "Read a newly released series", category: "DISCOVERY", icon: Icons.fiber_new),
AchievementModel(id: "early_witness", title: "EARLY WITNESS", desc: "Read within 24 hours of drop", category: "DISCOVERY", icon: Icons.access_time),
// RARE / HIDDEN
AchievementModel(id: "night_reader", title: "NIGHT READER", desc: "Read between 2AM-5AM", category: "RARE", icon: Icons.dark_mode),
AchievementModel(id: "marathon", title: "MARATHON", desc: "2+ hours in one session", category: "RARE", icon: Icons.timer),
AchievementModel(id: "silent_watcher", title: "SILENT WATCHER", desc: "Read 20 chapters with no comments", category: "RARE", icon: Icons.visibility_off),
AchievementModel(id: "break_loop", title: "BREAK THE LOOP", desc: "Return after 30 days inactive", category: "RARE", icon: Icons.loop),
AchievementModel(id: "og_member", title: "OG MEMBER", desc: "Joined during beta", category: "RARE", icon: Icons.auto_awesome),
AchievementModel(id: "founder_circle", title: "FOUNDER'S CIRCLE", desc: "Invited directly", category: "RARE", icon: Icons.stars),
AchievementModel(id: "system_maxed", title: "SYSTEM MAXED", desc: "Complete all reading achievements", category: "RARE", icon: Icons.diamond),
];
+31
View File
@@ -0,0 +1,31 @@
import 'package:cloud_firestore/cloud_firestore.dart';
class VigorManager {
static const int maxVigor = 3;
static const int rechargeHours = 3;
static Future<int> getAvailableVigor(Map<String, dynamic> userData) async {
int currentVigor = userData['vigor'] ?? maxVigor;
Timestamp? lastVigorRefill = userData['lastVigorRefill'];
if (lastVigorRefill == null) return currentVigor;
// Calculate time since last refill
DateTime lastTime = lastVigorRefill.toDate();
Duration diff = DateTime.now().difference(lastTime);
// If more than 3 hours have passed, full refill
if (diff.inHours >= rechargeHours) {
return maxVigor;
}
return currentVigor;
}
static Future<void> consumeVigor(String uid, int currentVigor) async {
await FirebaseFirestore.instance.collection('users').doc(uid).update({
'vigor': currentVigor - 1,
'lastVigorRefill': FieldValue.serverTimestamp(),
});
}
}
+104
View File
@@ -0,0 +1,104 @@
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:share_plus/share_plus.dart';
import 'package:onsolgo/core/constants.dart';
/// Captures a [RepaintBoundary] (via [key]) as PNG and shares it with [text] and a [link]
/// (defaults to [kOnsolAppWebUrl]). Uses in-memory bytes only (no [dart:io]).
class OnsolShare {
static Future<void> share(GlobalKey key, String text, {String? link}) =>
_shareImpl(key, text, link: link);
}
Future<void> _shareImpl(GlobalKey key, String text, {String? link}) async {
final url = (link ?? kOnsolAppWebUrl).trim();
String bodyWithLink(String raw) {
final caption = raw.trim().isEmpty ? 'ONSOL-GO!' : raw.trim();
if (caption.contains(url)) return caption;
return '$caption\n\n$url';
}
final fallbackMessage = bodyWithLink(text);
Future<void> shareText(String value) async {
await SharePlus.instance.share(ShareParams(text: value));
}
Future<void> clipboard(String value) async {
await Clipboard.setData(ClipboardData(text: value));
}
try {
final ctx = key.currentContext;
if (ctx == null || !ctx.mounted) {
await shareText(fallbackMessage);
return;
}
await SchedulerBinding.instance.endOfFrame;
if (!ctx.mounted) {
await shareText(fallbackMessage);
return;
}
final boundary = ctx.findRenderObject();
if (boundary is! RenderRepaintBoundary) {
await shareText(fallbackMessage);
return;
}
final image = await boundary.toImage(pixelRatio: 3.0);
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
await shareText(fallbackMessage);
return;
}
final pngBytes = byteData.buffer.asUint8List();
final file = XFile.fromData(
pngBytes,
mimeType: 'image/png',
name: 'onsol_share.png',
);
try {
await SharePlus.instance.share(
ShareParams(files: [file], text: bodyWithLink(text)),
);
} catch (e) {
if (kDebugMode) {
debugPrint('OnsolShare: file+caption share failed ($e), retrying file only');
}
try {
await SharePlus.instance.share(
ShareParams(files: [file], text: bodyWithLink(text)),
);
} catch (e2) {
if (kDebugMode) {
debugPrint('OnsolShare: file-only share failed ($e2), falling back to text');
}
await shareText(fallbackMessage);
}
}
} catch (e, st) {
if (kDebugMode) {
debugPrint('OnsolShare error: $e\n$st');
}
try {
await shareText(fallbackMessage);
} catch (e3) {
if (kDebugMode) {
debugPrint('OnsolShare text share failed: $e3');
}
try {
await clipboard(fallbackMessage);
} catch (_) {}
}
}
}
+34
View File
@@ -0,0 +1,34 @@
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:onsolgo/core/achievement_manager.dart';
class StreakManager {
static Future<void> updateStreak(String uid) async {
final docRef = FirebaseFirestore.instance.collection('users').doc(uid);
final snap = await docRef.get();
if (!snap.exists) return;
final data = snap.data() as Map<String, dynamic>;
final lastVisit = (data['lastVisit'] as Timestamp?)?.toDate();
int currentStreak = data['streak'] ?? 0;
final today = DateTime.now();
if (lastVisit != null) {
final diff = DateTime(today.year, today.month, today.day).difference(DateTime(lastVisit.year, lastVisit.month, lastVisit.day)).inDays;
if (diff == 1) {
currentStreak++;
await docRef.update({'streak': currentStreak, 'lastVisit': FieldValue.serverTimestamp()});
// Streak Unlocks
if (currentStreak >= 2) AchievementManager.unlock(uid, "back_tomorrow");
if (currentStreak >= 3) AchievementManager.unlock(uid, "3_day_discipline");
if (currentStreak >= 7) AchievementManager.unlock(uid, "weekly_ritual");
if (currentStreak >= 30) AchievementManager.unlock(uid, "monthly_reader");
if (currentStreak >= 60) AchievementManager.unlock(uid, "no_days_off");
} else if (diff > 1) {
await docRef.update({'streak': 1, 'lastVisit': FieldValue.serverTimestamp()});
}
} else {
await docRef.update({'streak': 1, 'lastVisit': FieldValue.serverTimestamp()});
AchievementManager.unlock(uid, "first_login");
}
}
}