Skip to content

Commit 8b08b1d

Browse files
authored
Merge pull request #114 from AmanSikarwar/v0.10.0
V0.10.0
2 parents 6e5ab0a + 5492ace commit 8b08b1d

23 files changed

Lines changed: 1478 additions & 207 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,5 @@ app.*.map.json
4545
/android/app/release
4646
keystore.base64
4747
android/build/reports/problems/problems-report.html
48+
49+
.planning/

android/app/build.gradle.kts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
33

44
plugins {
55
id("com.android.application")
6-
id("kotlin-android")
76
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
87
id("dev.flutter.flutter-gradle-plugin")
98
}
@@ -16,7 +15,7 @@ if (keystorePropertiesFile.exists()) {
1615

1716
android {
1817
namespace = "io.github.amansikarwar.freedium_mobile"
19-
compileSdk = flutter.compileSdkVersion
18+
compileSdk = 36
2019
ndkVersion = flutter.ndkVersion
2120

2221
compileOptions {
@@ -55,11 +54,12 @@ android {
5554
)
5655
}
5756
}
57+
compileSdkMinor = 1
5858
}
5959

6060
kotlin {
6161
compilerOptions {
62-
jvmTarget.set(JvmTarget.JVM_17)
62+
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
6363
}
6464
}
6565

android/gradle.properties

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,5 @@ android.usesSdkInManifest.disallowed=false
88
android.uniquePackageNames=false
99
android.dependency.useConstraints=true
1010
android.r8.strictFullModeForKeepRules=false
11-
android.r8.optimizedResourceShrinking=false
1211
android.builtInKotlin=false
1312
android.newDsl=false

android/settings.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pluginManagement {
1818

1919
plugins {
2020
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
21-
id("com.android.application") version "9.1.0" apply false
21+
id("com.android.application") version "9.1.1" apply false
2222
}
2323

2424
include(":app")

assets/js/theme.js

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,32 +74,24 @@
7474
customCSS.textContent = `%CUSTOM_CSS_CONTENT%`;
7575
document.head.appendChild(customCSS);
7676

77+
// Use the same HLJS version (11.9.0) that Freedium loads in its <head>.
78+
// Pointing to a different version would load a second HLJS script and
79+
// cause the stylesheet URL to be replaced with an unmatched version.
7780
const desiredHljsTheme = isDarkMode === "true" ? "github-dark" : "github";
78-
const hljsThemeUrl = `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/${desiredHljsTheme}.min.css`;
81+
const hljsThemeUrl = `https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/${desiredHljsTheme}.min.css`;
7982
try {
80-
const existingLink = document.querySelector(
81-
'link[href*="highlight.js"][href*="styles"]'
82-
);
83-
if (existingLink && !existingLink.href.includes(desiredHljsTheme)) {
84-
const preloadLink = document.createElement("link");
85-
preloadLink.rel = "preload";
86-
preloadLink.as = "style";
87-
preloadLink.href = hljsThemeUrl;
88-
preloadLink.onload = function () {
89-
existingLink.href = hljsThemeUrl;
90-
preloadLink.remove();
91-
};
92-
preloadLink.onerror = function () {
93-
existingLink.href = hljsThemeUrl;
94-
preloadLink.remove();
95-
};
96-
document.head.appendChild(preloadLink);
97-
} else if (!existingLink) {
98-
const link = document.createElement("link");
99-
link.rel = "stylesheet";
100-
link.href = hljsThemeUrl;
101-
document.head.appendChild(link);
102-
}
83+
// Freedium's page script already loaded a highlight.js stylesheet.
84+
// We swap its href so the correct light/dark theme is applied.
85+
// Remove ALL existing HLJS style links first to avoid duplicates.
86+
document
87+
.querySelectorAll('link[href*="highlight.js"][href*="styles"]')
88+
.forEach(function (el) {
89+
el.remove();
90+
});
91+
const link = document.createElement("link");
92+
link.rel = "stylesheet";
93+
link.href = hljsThemeUrl;
94+
document.head.appendChild(link);
10395
} catch (e) {
10496
console.warn("Failed to set HLJS theme:", e);
10597
}
@@ -265,6 +257,70 @@
265257
} catch (e) {
266258
console.warn("Failed to call Flutter handler:", e);
267259
}
260+
261+
// Article metadata extraction — best-effort, does not affect reading experience.
262+
// Selectors verified against the actual Freedium HTML structure.
263+
setTimeout(function () {
264+
try {
265+
// ── Title ──────────────────────────────────────────────────────────
266+
// Prefer the <h1> inside the .font-sans wrapper (the article header).
267+
// Fall back to <title>, stripping Freedium's suffix.
268+
var titleEl = document.querySelector("div.font-sans > h1") ||
269+
document.querySelector("h1");
270+
var title = titleEl
271+
? titleEl.innerText.trim()
272+
: document.title
273+
.replace(/ [|\-] Freedium$/i, "")
274+
.replace(/ by .+ - Freedium$/i, "")
275+
.trim();
276+
277+
// ── Author ─────────────────────────────────────────────────────────
278+
// The author card: div.bg-gray-100 > div.flex > div.flex-grow > a
279+
// Specifically the first <a> linking to medium.com inside .flex-grow.
280+
var authorEl =
281+
document.querySelector("div.flex-grow > a[href*='medium.com']");
282+
var author = authorEl ? authorEl.innerText.trim() : "";
283+
284+
// ── Read time ──────────────────────────────────────────────────────
285+
// Freedium renders read time as a plain <span> containing "min read".
286+
// There is no data-testid or class that uniquely identifies it.
287+
var readTime = "";
288+
var spans = document.querySelectorAll(
289+
"div.flex.flex-wrap.items-center span"
290+
);
291+
for (var i = 0; i < spans.length; i++) {
292+
var txt = spans[i].innerText || "";
293+
if (txt.includes("min read")) {
294+
readTime = txt.trim();
295+
break;
296+
}
297+
}
298+
299+
// ── Hero image ─────────────────────────────────────────────────────
300+
// Freedium places a preview image with alt="Preview image" near the top.
301+
// Fall back to the first non-data image inside the .font-sans wrapper.
302+
var heroImg = "";
303+
var heroEl =
304+
document.querySelector("img[alt='Preview image']") ||
305+
document.querySelector("div.font-sans img");
306+
if (heroEl && heroEl.src && !heroEl.src.startsWith("data:")) {
307+
heroImg = heroEl.src;
308+
}
309+
310+
if (window.ArticleMeta && window.ArticleMeta.postMessage) {
311+
window.ArticleMeta.postMessage(
312+
JSON.stringify({
313+
title: title,
314+
author: author,
315+
readTime: readTime,
316+
heroImageUrl: heroImg,
317+
})
318+
);
319+
}
320+
} catch (e) {
321+
console.warn("ArticleMeta extraction failed:", e);
322+
}
323+
}, 800);
268324
} catch (e) {
269325
console.error("Theme application failed:", e);
270326
try {

lib/app.dart

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import 'dart:async';
2+
13
import 'package:flutter/material.dart';
2-
import 'package:flutter/scheduler.dart';
34
import 'package:flutter_riverpod/flutter_riverpod.dart';
45
import 'package:freedium_mobile/core/constants/app_constants.dart';
56
import 'package:freedium_mobile/core/services/intent_service.dart';
67
import 'package:freedium_mobile/core/theme/theme_provider.dart';
78
import 'package:freedium_mobile/features/home/presentation/home_screen.dart';
9+
import 'package:freedium_mobile/features/onboarding/application/onboarding_provider.dart';
10+
import 'package:freedium_mobile/features/onboarding/presentation/onboarding_screen.dart';
811
import 'package:freedium_mobile/features/webview/presentation/webview_screen.dart';
912
import 'package:listen_sharing_intent/listen_sharing_intent.dart';
1013

@@ -24,6 +27,24 @@ final initialIntentHandledProvider =
2427
InitialIntentHandledNotifier.new,
2528
);
2629

30+
class PendingIntentUrlNotifier extends Notifier<String?> {
31+
@override
32+
String? build() => null;
33+
34+
void stash(String url) {
35+
state = url;
36+
}
37+
38+
void clear() {
39+
state = null;
40+
}
41+
}
42+
43+
final pendingIntentUrlProvider =
44+
NotifierProvider<PendingIntentUrlNotifier, String?>(
45+
PendingIntentUrlNotifier.new,
46+
);
47+
2748
class App extends ConsumerWidget {
2849
const App({super.key});
2950

@@ -46,46 +67,59 @@ class App extends ConsumerWidget {
4667
}
4768
}
4869

70+
void _handleIncomingIntent(WidgetRef ref, String url) {
71+
if (url.isEmpty) return;
72+
73+
final onboarding = ref.read(onboardingProvider);
74+
if (onboarding.isLoading || !onboarding.hasSeenOnboarding) {
75+
ref.read(pendingIntentUrlProvider.notifier).stash(url);
76+
return;
77+
}
78+
79+
_navigateToWebview(url);
80+
ReceiveSharingIntent.instance.reset();
81+
}
82+
83+
Future<void> _processInitialIntent(WidgetRef ref) async {
84+
await Future<void>.delayed(const Duration(milliseconds: 400));
85+
final value = await ref.read(intentServiceProvider).getInitialIntent();
86+
if (value.isEmpty) return;
87+
88+
final url = value.first.path;
89+
if (url.isEmpty) return;
90+
91+
_handleIncomingIntent(ref, url);
92+
}
93+
4994
@override
5095
Widget build(BuildContext context, WidgetRef ref) {
5196
final themeAsync = ref.watch(dynamicThemeProvider);
5297
final themeMode = ref.watch(themeModeProvider);
5398
final hasHandledInitialIntent = ref.watch(initialIntentHandledProvider);
99+
final onboarding = ref.watch(onboardingProvider);
100+
final hasSeenOnboarding = onboarding.hasSeenOnboarding;
101+
102+
ref.listen<OnboardingState>(onboardingProvider, (previous, next) {
103+
if (next.isLoading || !next.hasSeenOnboarding) return;
104+
105+
final pendingUrl = ref.read(pendingIntentUrlProvider);
106+
if (pendingUrl == null || pendingUrl.isEmpty) return;
107+
108+
ref.read(pendingIntentUrlProvider.notifier).clear();
109+
_navigateToWebview(pendingUrl);
110+
ReceiveSharingIntent.instance.reset();
111+
});
54112

55113
ref.listen<AsyncValue<String>>(intentStreamProvider, (previous, next) {
56114
next.whenData((url) {
57-
if (url.isNotEmpty) {
58-
final navigator = navigatorKey.currentState;
59-
if (navigator != null && navigator.context.mounted) {
60-
final currentRoute = ModalRoute.of(navigator.context);
61-
final isCurrentlyOnWebview =
62-
currentRoute?.settings.name?.startsWith('/webview/') ?? false;
63-
64-
if (!isCurrentlyOnWebview) {
65-
_navigateToWebview(url);
66-
ReceiveSharingIntent.instance.reset();
67-
}
68-
}
69-
}
115+
_handleIncomingIntent(ref, url);
70116
});
71117
});
72118

73119
if (!hasHandledInitialIntent) {
74120
WidgetsBinding.instance.addPostFrameCallback((_) {
75121
ref.read(initialIntentHandledProvider.notifier).setHandled();
76-
ref.read(intentServiceProvider).getInitialIntent().then((value) {
77-
if (value.isNotEmpty) {
78-
final url = value.first.path;
79-
if (url.isNotEmpty) {
80-
SchedulerBinding.instance.addPostFrameCallback((_) {
81-
Future.delayed(const Duration(milliseconds: 200), () {
82-
_navigateToWebview(url);
83-
ReceiveSharingIntent.instance.reset();
84-
});
85-
});
86-
}
87-
}
88-
});
122+
unawaited(_processInitialIntent(ref));
89123
});
90124
}
91125

@@ -96,7 +130,11 @@ class App extends ConsumerWidget {
96130
theme: theme.lightTheme,
97131
darkTheme: theme.darkTheme,
98132
themeMode: themeMode,
99-
home: const HomeScreen(),
133+
home: onboarding.isLoading
134+
? const Scaffold(body: Center(child: CircularProgressIndicator()))
135+
: hasSeenOnboarding
136+
? const HomeScreen()
137+
: const OnboardingScreen(),
100138
),
101139
loading: () => const MaterialApp(
102140
home: Scaffold(body: Center(child: CircularProgressIndicator())),

lib/core/constants/app_constants.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class AppConstants {
77
static const String appSourceUrl = 'https://github.com/amansikarwar/freedium';
88
static const String appVersion = .fromEnvironment(
99
'APP_VERSION',
10-
defaultValue: '0.9.0',
10+
defaultValue: '0.10.0',
1111
);
1212

1313
static const String urlRegExp =

0 commit comments

Comments
 (0)