import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_sharing_intent/flutter_sharing_intent.dart'; import 'package:flutter_sharing_intent/model/sharing_file.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shlink_app/API/Classes/ShlinkStats/shlink_stats.dart'; import 'package:shlink_app/API/server_manager.dart'; import 'package:shlink_app/main.dart'; import 'package:shlink_app/views/login_view.dart'; import 'package:shlink_app/views/short_url_edit_view.dart'; import 'package:shlink_app/views/url_list_view.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../API/Classes/ShortURL/short_url.dart'; import '../globals.dart' as globals; class HomeView extends StatefulWidget { const HomeView({super.key}); @override State createState() => _HomeViewState(); } class _HomeViewState extends State { ShlinkStats? shlinkStats; List shortUrls = []; bool shortUrlsLoaded = false; bool _qrCodeShown = false; String _qrUrl = ""; late StreamSubscription _intentDataStreamSubscription; @override void initState() { super.initState(); initializeActionProcessText(); WidgetsBinding.instance.addPostFrameCallback((_) { loadAllData(); }); } Future initializeActionProcessText() async { _intentDataStreamSubscription = FlutterSharingIntent.instance.getMediaStream().listen(_handleIntentUrl); FlutterSharingIntent.instance.getInitialSharing().then(_handleIntentUrl); } Future _handleIntentUrl(List value) async { String inputUrlText = value.firstOrNull?.value ?? ""; if (await canLaunchUrlString(inputUrlText)) { await Navigator.of(context).push(MaterialPageRoute( builder: (context) => ShortURLEditView(longUrl: inputUrlText))); await loadAllData(); } } Future loadAllData() async { await loadShlinkStats(); await loadRecentShortUrls(); return; } Future loadShlinkStats() async { final response = await globals.serverManager.getShlinkStats(); response.fold((l) { setState(() { shlinkStats = l; }); }, (r) { var text = ""; if (r is RequestFailure) { text = r.description; } else { text = (r as ApiFailure).detail; } final snackBar = SnackBar( content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating); ScaffoldMessenger.of(context).showSnackBar(snackBar); }); } Future loadRecentShortUrls() async { final response = await globals.serverManager.getRecentShortUrls(); response.fold((l) { setState(() { shortUrls = l; shortUrlsLoaded = true; }); }, (r) { var text = ""; if (r is RequestFailure) { text = r.description; } else { text = (r as ApiFailure).detail; } final snackBar = SnackBar( content: Text(text), backgroundColor: Colors.red[400], behavior: SnackBarBehavior.floating); ScaffoldMessenger.of(context).showSnackBar(snackBar); }); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: [ ColorFiltered( colorFilter: ColorFilter.mode( Colors.black.withOpacity(_qrCodeShown ? 0.4 : 0), BlendMode.srcOver), child: RefreshIndicator( onRefresh: () async { return loadAllData(); }, child: CustomScrollView( slivers: [ SliverAppBar.medium( automaticallyImplyLeading: false, expandedHeight: 160, title: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("Shlink", style: TextStyle(fontWeight: FontWeight.bold)), GestureDetector( onTap: () { showModalBottomSheet( context: context, builder: (BuildContext context) { return AvailableServerBottomSheet(); }); }, child: Text(globals.serverManager.getServerUrl(), style: TextStyle( fontSize: 16, color: Colors.grey[600])), ) ], )), SliverToBoxAdapter( child: Wrap( alignment: WrapAlignment.spaceEvenly, children: [ _ShlinkStatsCardWidget( icon: Icons.link, text: "${shlinkStats?.shortUrlsCount.toString() ?? "0"} Short URLs", borderColor: Colors.blue), _ShlinkStatsCardWidget( icon: Icons.remove_red_eye, text: "${shlinkStats?.nonOrphanVisits.total ?? "0"} Visits", borderColor: Colors.green), _ShlinkStatsCardWidget( icon: Icons.warning, text: "${shlinkStats?.orphanVisits.total ?? "0"} Orphan Visits", borderColor: Colors.red), _ShlinkStatsCardWidget( icon: Icons.sell, text: "${shlinkStats?.tagsCount.toString() ?? "0"} Tags", borderColor: Colors.purple), ], ), ), if (shortUrlsLoaded && shortUrls.isEmpty) SliverToBoxAdapter( child: Center( child: Padding( padding: const EdgeInsets.only(top: 50), child: Column( children: [ const Text( "No Short URLs", style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold), ), Padding( padding: const EdgeInsets.only(top: 8), child: Text( 'Create one by tapping the "+" button below', style: TextStyle( fontSize: 16, color: Colors.grey[600]), ), ) ], )))) else SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { if (index == 0) { return const Padding( padding: EdgeInsets.only(top: 16, left: 12, right: 12), child: Text("Recent Short URLs", style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold)), ); } else { final shortURL = shortUrls[index - 1]; return ShortURLCell( shortURL: shortURL, reload: () { loadRecentShortUrls(); }, showQRCode: (String url) { setState(() { _qrUrl = url; _qrCodeShown = true; }); }, isLast: index == shortUrls.length); } }, childCount: shortUrls.length + 1)) ], ), ), ), if (_qrCodeShown) GestureDetector( onTap: () { setState(() { _qrCodeShown = false; }); }, child: Container( color: Colors.black.withOpacity(0), ), ), if (_qrCodeShown) Center( child: SizedBox( width: MediaQuery.of(context).size.width / 1.7, height: MediaQuery.of(context).size.width / 1.7, child: Card( child: Padding( padding: const EdgeInsets.all(16), child: QrImageView( data: _qrUrl, size: 200.0, eyeStyle: QrEyeStyle( eyeShape: QrEyeShape.square, color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.white : Colors.black, ), dataModuleStyle: QrDataModuleStyle( dataModuleShape: QrDataModuleShape.square, color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.white : Colors.black, ), ))), ), ) ], ), floatingActionButton: FloatingActionButton( onPressed: () async { await Navigator.of(context).push(MaterialPageRoute( builder: (context) => const ShortURLEditView())); loadRecentShortUrls(); }, child: const Icon(Icons.add), )); } } // stats card widget class _ShlinkStatsCardWidget extends StatefulWidget { const _ShlinkStatsCardWidget( {required this.text, required this.icon, this.borderColor}); final IconData icon; final Color? borderColor; final String text; @override State<_ShlinkStatsCardWidget> createState() => _ShlinkStatsCardWidgetState(); } class _ShlinkStatsCardWidgetState extends State<_ShlinkStatsCardWidget> { @override Widget build(BuildContext context) { var randomColor = ([...Colors.primaries]..shuffle()).first; return Padding( padding: const EdgeInsets.all(4), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border.all(color: widget.borderColor ?? randomColor), borderRadius: BorderRadius.circular(8)), child: SizedBox( child: Wrap( children: [ Icon(widget.icon), Padding( padding: const EdgeInsets.only(left: 4), child: Text(widget.text, style: const TextStyle(fontWeight: FontWeight.bold)), ) ], ), )), ); } } class AvailableServerBottomSheet extends StatefulWidget { const AvailableServerBottomSheet({super.key}); @override State createState() => _AvailableServerBottomSheetState(); } class _AvailableServerBottomSheetState extends State { List availableServers = []; @override void initState() { super.initState(); _loadServers(); } Future _loadServers() async { List _availableServers = await globals.serverManager.getAvailableServers(); setState(() { availableServers = _availableServers; }); } @override Widget build(BuildContext context) { return CustomScrollView( slivers: [ const SliverAppBar.medium( expandedHeight: 120, automaticallyImplyLeading: false, title: Text( "Available Servers", style: TextStyle(fontWeight: FontWeight.bold), ), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return GestureDetector( onTap: () async { final prefs = await SharedPreferences.getInstance(); prefs.setString("lastusedserver", availableServers[index]); await Navigator.of(context) .pushAndRemoveUntil(MaterialPageRoute( builder: (context) => InitialPage()), (Route route) => false); }, child: Padding( padding: EdgeInsets.only(left: 8, right: 8), child: Container( padding: EdgeInsets.only(left: 8, right: 8, top: 16, bottom: 16), decoration: BoxDecoration( border: Border( bottom: BorderSide( color: MediaQuery.of(context).platformBrightness == Brightness.dark ? Colors.grey[800]! : Colors.grey[300]!)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Wrap( spacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Icon(Icons.dns_outlined), Text(availableServers[index]) ], ), Wrap( crossAxisAlignment: WrapCrossAlignment.center, children: [ if (availableServers[index] == globals.serverManager.serverUrl) Container( width: 8, height: 8, decoration: BoxDecoration( color: Colors.green, borderRadius: BorderRadius.circular(4) ), ), IconButton( onPressed: () async { globals.serverManager.logOut(availableServers[index]); if (availableServers[index] == globals.serverManager.serverUrl) { await Navigator.of(context) .pushAndRemoveUntil(MaterialPageRoute( builder: (context) => InitialPage()), (Route route) => false); } else { Navigator.pop(context); } }, icon: Icon(Icons.logout, color: Colors.red), ) ], ) ], ) )), ); }, childCount: availableServers.length )), SliverToBoxAdapter( child: Padding( padding: EdgeInsets.only(top: 8), child: Center( child: ElevatedButton( onPressed: () async { await Navigator.of(context) .push(MaterialPageRoute( builder: (context) => LoginView())); }, child: Text("Add server..."), ), ), ) ) ], ); } }