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:
@@ -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,
|
||||
|
||||
325
firka/lib/ui/phone/widgets/domain_browser_webview.dart
Normal file
325
firka/lib/ui/phone/widgets/domain_browser_webview.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user