1
0
forked from firka/firka

feat: add privacy policy to domain browser, and add color change for url, and show url

This commit is contained in:
zypherift
2026-03-05 17:22:54 +01:00
parent 32a6452c1b
commit d0a517ae1e
4 changed files with 386 additions and 19 deletions

View File

@@ -5,6 +5,7 @@ import 'package:carousel_slider/carousel_slider.dart';
import 'package:firka/core/firka_bundle.dart';
import 'package:firka/app/app_state.dart';
import 'package:flutter/foundation.dart';
import 'package:firka/ui/phone/widgets/domain_browser_webview.dart';
import 'package:firka/ui/phone/widgets/login_webview.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -12,7 +13,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:sensors_plus/sensors_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:vibration/vibration.dart';
import 'package:firka/core/bloc/theme_cubit.dart';
@@ -21,9 +21,8 @@ import 'package:firka/core/image_preloader.dart';
import 'package:firka/ui/theme/style.dart';
import 'package:firka/ui/shared/delayed_spinner.dart';
const String _privacyUrlHungarian =
'https://github.com/QwIT-Development/privacy-policy/blob/master/README.md';
const String _privacyUrlOther = 'https://firka.app/privacy';
const String _privacyUrlHungarian = 'https://firka.app/privacy-policy';
const String _privacyUrlOther = 'https://firka.app/privacy-policy';
class LoginScreen extends StatefulWidget {
final AppInitialization data;
@@ -45,17 +44,27 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
}
String _getPrivacyPolicyUrl() {
final locale = Localizations.localeOf(context).languageCode;
return locale == 'hu' ? _privacyUrlHungarian : _privacyUrlOther;
return "https://firka.app/privacy";
}
Future<void> _launchPrivacyPolicy() async {
Future<void> _showPrivacyPolicyWebview() async {
final url = _getPrivacyPolicyUrl();
try {
await launchUrl(Uri.parse(url));
} catch (e) {
logger.shout('LoginScreen: Error launching privacy policy URL: $e');
}
if (!mounted) return;
await showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
showDragHandle: false,
builder: (BuildContext context) {
return SizedBox(
height: MediaQuery.sizeOf(context).height,
child: DomainBrowserWebviewWidget(
data: widget.data,
url: url,
),
);
},
);
}
Future<void> _preloadImages() async {
@@ -513,7 +522,7 @@ class _LoginScreenState extends FirkaState<LoginScreen> {
),
const SizedBox(height: 20),
GestureDetector(
onTap: _launchPrivacyPolicy,
onTap: _showPrivacyPolicyWebview,
child: Text(
widget.data.l10n.privacyLabel,
textAlign: TextAlign.center,

View File

@@ -0,0 +1,325 @@
import 'dart:async';
import 'package:firka/app/app_state.dart';
import 'package:firka/core/state/firka_state.dart';
import 'package:firka/ui/theme/style.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:majesticons_flutter/majesticons_flutter.dart';
import 'package:webview_flutter/webview_flutter.dart';
/// Lightweight in-app browser used outside the login flow (e.g. privacy policy).
///
/// This deliberately contains only generic WebView behaviour, keeping all
/// login/token handling inside `login_webview.dart`.
class DomainBrowserWebviewWidget extends StatefulWidget {
final AppInitialization? data;
final String? url;
const DomainBrowserWebviewWidget({
super.key,
this.data,
this.url,
});
@override
State<DomainBrowserWebviewWidget> createState() =>
_DomainBrowserWebviewWidgetState();
}
class _DomainBrowserWebviewWidgetState
extends FirkaState<DomainBrowserWebviewWidget>
with TickerProviderStateMixin {
late WebViewController _webViewController;
bool _isLoading = true;
AnimationController? _fadeAnimationController;
Animation<double>? _fadeAnimation;
@override
void initState() {
super.initState();
_fadeAnimationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(_fadeAnimationController!);
assert(widget.data != null && widget.url != null,
'DomainBrowserWebviewWidget requires non-null data and url');
_webViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..loadRequest(Uri.parse(widget.url!))
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (String url) {
Timer(const Duration(milliseconds: 300), () {
if (mounted) {
setState(() {
_isLoading = false;
});
_fadeAnimationController?.forward().then((_) {
_fadeAnimationController?.reset();
});
}
});
},
onPageStarted: (String url) {
setState(() {
_isLoading = true;
});
_fadeAnimationController?.reset();
},
),
);
}
@override
void dispose() {
_fadeAnimationController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (widget.data == null || widget.url == null) {
return const SizedBox.shrink();
}
final data = widget.data!;
final mediaQuery = MediaQuery.of(context);
final safePadding = mediaQuery.padding;
final displayUrl = (widget.url ?? '').replaceFirst(RegExp(r'^https?://'), '');
final displayParts = displayUrl.split('/');
final host = displayParts.isNotEmpty ? displayParts.first : displayUrl;
final path = displayParts.length > 1
? '/${displayParts.sublist(1).join('/')}'
: '';
return Material(
color: appStyle.colors.background,
child: Padding(
padding: EdgeInsets.only(
top: 61 + safePadding.top,
left: 12,
right: 12,
bottom: safePadding.bottom,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 2),
child: SvgPicture.asset(
"assets/icons/dave.svg",
width: 24,
height: 24,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.data?.l10n.runningInDomainBrowser ??
'Domain Browser',
style: appStyle.fonts.B_16R.copyWith(
color: appStyle.colors.textPrimary,
),
),
),
const SizedBox(width: 8),
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: 36,
height: 36,
alignment: Alignment.center,
decoration: BoxDecoration(
color: appStyle.colors.buttonSecondaryFill,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: appStyle.colors.shadowColor,
offset: const Offset(0, 1),
blurRadius: appStyle.colors.shadowBlur.toDouble(),
),
],
),
child: Majesticon(
Majesticon.multiplySolid,
color: appStyle.colors.accent,
size: 16,
),
),
),
],
),
const SizedBox(height: 22),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
WebViewWidget(
controller: _webViewController,
gestureRecognizers: {
Factory<OneSequenceGestureRecognizer>(
() => EagerGestureRecognizer(),
),
},
),
if (_fadeAnimationController != null &&
_fadeAnimation != null)
IgnorePointer(
ignoring: !_isLoading,
child: AnimatedBuilder(
animation: _fadeAnimationController!,
builder: (context, child) => AnimatedOpacity(
opacity: _isLoading
? 1.0
: _fadeAnimationController!.isAnimating
? _fadeAnimation!.value
: 0.0,
duration: const Duration(milliseconds: 500),
child: Container(
color: appStyle.colors.background,
child: Center(
child: Image.asset(
"assets/images/logos/loading.gif",
width: 50,
height: 50,
),
),
),
),
),
),
],
),
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Container(
height: 42,
decoration: BoxDecoration(
color: appStyle.colors.card,
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Align(
alignment: Alignment.centerLeft,
child: RichText(
text: TextSpan(
text: host,
style: appStyle.fonts.B_14R.copyWith(
fontSize: 16,
color: appStyle.colors.textPrimary,
),
children: [
TextSpan(
text: path,
style: appStyle.fonts.B_14R.copyWith(
fontSize: 16,
color: appStyle.colors.textTeritary ??
appStyle.colors.textSecondary,
),
),
],
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),
),
const SizedBox(width: 8),
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: appStyle.colors.buttonSecondaryFill,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: appStyle.colors.shadowColor,
offset: const Offset(0, 1),
blurRadius: appStyle.colors.shadowBlur.toDouble(),
),
],
),
child: Center(
child: Image.asset(
"assets/icons/button/colorwheel.png",
width: 22,
height: 22,
),
),
),
const SizedBox(width: 8),
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: appStyle.colors.buttonSecondaryFill,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: appStyle.colors.shadowColor,
offset: const Offset(0, 1),
blurRadius: appStyle.colors.shadowBlur.toDouble(),
),
],
),
child: Center(
child: Majesticon(
Majesticon.chevronLeftLine,
color: appStyle.colors.secondary,
size: 22,
),
),
),
const SizedBox(width: 8),
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: appStyle.colors.buttonSecondaryFill,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: appStyle.colors.shadowColor,
offset: const Offset(0, 1),
blurRadius: appStyle.colors.shadowBlur.toDouble(),
),
],
),
child: Center(
child: Majesticon(
Majesticon.menuLine,
color: appStyle.colors.secondary,
size: 22,
),
),
),
],
),
const SizedBox(height: 16),
],
),
),
);
}
}

View File

@@ -5,6 +5,8 @@ import 'package:firka/data/models/app_settings_model.dart';
import 'package:firka/services/live_activity_service.dart';
import 'package:firka/app/app_state.dart';
import 'package:firka/app/initialization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar_community/isar.dart';
@@ -41,6 +43,8 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
bool _isLoading = true;
AnimationController? _fadeAnimationController;
Animation<double>? _fadeAnimation;
late String _displayHost;
late String _displayPath;
@override
void initState() {
@@ -65,6 +69,11 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
);
}
final trimmed = loginUrl.replaceFirst(RegExp(r'^https?://'), '');
final parts = trimmed.split('/');
_displayHost = parts.isNotEmpty ? parts.first : trimmed;
_displayPath = parts.length > 1 ? '/${parts.sublist(1).join('/')}' : '';
logger.info("Using loginUrl: $loginUrl");
_webViewController = WebViewController()
@@ -259,7 +268,14 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
borderRadius: BorderRadius.circular(20),
child: Stack(
children: [
WebViewWidget(controller: _webViewController),
WebViewWidget(
controller: _webViewController,
gestureRecognizers: {
Factory<OneSequenceGestureRecognizer>(
() => EagerGestureRecognizer(),
),
},
),
if (_fadeAnimationController != null &&
_fadeAnimation != null)
IgnorePointer(
@@ -302,16 +318,29 @@ class _LoginWebviewWidgetState extends FirkaState<LoginWebviewWidget>
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Row(
children: [
Text(
"eKréta/Bejelentkezés",
child: Align(
alignment: Alignment.centerLeft,
child: RichText(
text: TextSpan(
text: _displayHost,
style: appStyle.fonts.B_14R.copyWith(
fontSize: 16,
color: appStyle.colors.textPrimary,
),
children: [
TextSpan(
text: _displayPath,
style: appStyle.fonts.B_14R.copyWith(
fontSize: 16,
color: appStyle.colors.textTeritary ??
appStyle.colors.textSecondary,
),
),
],
),
],
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
),

View File

@@ -56,6 +56,7 @@ class FirkaColors {
Color textPrimary;
Color textSecondary;
Color textTertiary;
Color? textTeritary;
Color textPrimaryLight;
Color textSecondaryLight;
@@ -97,6 +98,7 @@ class FirkaColors {
required this.textPrimary,
required this.textSecondary,
required this.textTertiary,
this.textTeritary,
required this.textPrimaryLight,
required this.textSecondaryLight,
required this.textTertiaryLight,
@@ -238,6 +240,7 @@ final FirkaStyle lightStyle = FirkaStyle(
textPrimary: Color(0xFF394C0A),
textSecondary: Color(0xCC394C0A),
textTertiary: Color(0x80394C0A),
textTeritary: Color(0xFF97A474),
textPrimaryLight: Color(0xFF394C0A),
textSecondaryLight: Color(0xCC394C0A),
textTertiaryLight: Color(0x80394C0A),
@@ -277,6 +280,7 @@ final FirkaStyle darkStyle = FirkaStyle(
textPrimary: Color(0xFFEAF7CC),
textSecondary: Color(0xB3EAF7CC),
textTertiary: Color(0x80EAF7CC),
textTeritary: Color(0xFF97A474),
textPrimaryLight: Color(0xFF394C0A),
textSecondaryLight: Color(0xCC394C0A),
textTertiaryLight: Color(0x80394C0A),