import 'package:audio_service/audio_service.dart'; import 'package:country_pickers/country.dart'; import 'package:country_pickers/country_picker_dialog.dart'; import 'package:filesize/filesize.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; import 'package:fluttericon/font_awesome5_icons.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:freezer/api/cache.dart'; import 'package:freezer/api/deezer.dart'; import 'package:freezer/api/download.dart'; import 'package:freezer/api/player.dart'; import 'package:freezer/ui/downloads_screen.dart'; import 'package:freezer/ui/elements.dart'; import 'package:freezer/ui/error.dart'; import 'package:freezer/ui/home_screen.dart'; import 'package:freezer/ui/updater.dart'; import 'package:i18n_extension/i18n_widget.dart'; import 'package:language_pickers/language_pickers.dart'; import 'package:language_pickers/languages.dart'; import 'package:package_info/package_info.dart'; import 'package:path_provider_ex/path_provider_ex.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:freezer/translations.i18n.dart'; import 'package:clipboard/clipboard.dart'; import 'package:scrobblenaut/scrobblenaut.dart'; import 'package:url_launcher/url_launcher.dart'; import '../settings.dart'; import '../main.dart'; import 'dart:io'; class SettingsScreen extends StatefulWidget { @override _SettingsScreenState createState() => _SettingsScreenState(); } class _SettingsScreenState extends State { List> _languages() { //Missing language defaultLanguagesList.add({ 'name': 'Filipino', 'isoCode': 'fil' }); List> _l = supportedLocales.map>((l) { Map _lang = defaultLanguagesList.firstWhere((lang) => lang['isoCode'] == l.languageCode); return { 'name': _lang['name'], 'isoCode': _lang['isoCode'], 'locale': l.toString() }; }).toList(); return _l; } @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Settings'.i18n), body: ListView( children: [ ListTile( title: Text('General'.i18n), leading: LeadingIcon(Icons.settings, color: Color(0xffeca704)), onTap: () => Navigator.of(context).push(MaterialPageRoute( builder: (context) => GeneralSettings() )), ), ListTile( title: Text('Download Settings'.i18n), leading: LeadingIcon(Icons.cloud_download, color: Color(0xffbe3266)), onTap: () => Navigator.of(context).push(MaterialPageRoute( builder: (context) => DownloadsSettings() )), ), ListTile( title: Text('Appearance'.i18n), leading: LeadingIcon(Icons.color_lens, color: Color(0xff4b2e7e)), onTap: () => Navigator.push( context, MaterialPageRoute(builder: (context) => AppearanceSettings()) ), ), ListTile( title: Text('Quality'.i18n), leading: LeadingIcon(Icons.high_quality, color: Color(0xff384697)), onTap: () => Navigator.push( context, MaterialPageRoute(builder: (context) => QualitySettings()) ), ), ListTile( title: Text('Deezer'.i18n), leading: LeadingIcon(Icons.equalizer, color: Color(0xff0880b5)), onTap: () => Navigator.push(context, MaterialPageRoute( builder: (context) => DeezerSettings() )), ), //Language select ListTile( title: Text('Language'.i18n), leading: LeadingIcon(Icons.language, color: Color(0xff009a85)), onTap: () { showDialog( context: context, builder: (context) => SimpleDialog( title: Text('Select language'.i18n), children: List.generate(_languages().length, (int i) { Map l = _languages()[i]; return ListTile( title: Text(l['name']), subtitle: Text(l['locale']), onTap: () async { setState(() => settings.language = l['locale']); await settings.save(); showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Language'.i18n), content: Text('Language changed, please restart Freezer to apply!'.i18n), actions: [ FlatButton( child: Text('OK'), onPressed: () { Navigator.of(context).pop(); Navigator.of(context).pop(); }, ) ], ); } ); }, ); }) ) ); }, ), ListTile( title: Text('Updates'.i18n), leading: LeadingIcon(Icons.update, color: Color(0xff2ba766)), onTap: () => Navigator.push(context, MaterialPageRoute( builder: (context) => UpdaterScreen() )), ), ListTile( title: Text('About'.i18n), leading: LeadingIcon(Icons.info, color: Colors.grey), onTap: () => Navigator.push(context, MaterialPageRoute( builder: (context) => CreditsScreen() )), ), ], ), ); } } class AppearanceSettings extends StatefulWidget { @override _AppearanceSettingsState createState() => _AppearanceSettingsState(); } class _AppearanceSettingsState extends State { ColorSwatch _swatch(int c) => ColorSwatch(c, {500: Color(c)}); @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Appearance'.i18n), body: ListView( children: [ ListTile( title: Text('Theme'.i18n), subtitle: Text('Currently'.i18n + ': ${settings.theme.toString().split('.').last}'), leading: Icon(Icons.color_lens), onTap: () { showDialog( context: context, builder: (context) { return SimpleDialog( title: Text('Select theme'.i18n), children: [ SimpleDialogOption( child: Text('Light'.i18n), onPressed: () { setState(() => settings.theme = Themes.Light); settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Dark'.i18n), onPressed: () { setState(() => settings.theme = Themes.Dark); settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Black (AMOLED)'.i18n), onPressed: () { setState(() => settings.theme = Themes.Black); settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), SimpleDialogOption( child: Text('Deezer (Dark)'.i18n), onPressed: () { setState(() => settings.theme = Themes.Deezer); settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), ], ); } ); }, ), ListTile( title: Text('Use system theme'.i18n), trailing: Switch( value: settings.useSystemTheme, onChanged: (bool v) async { setState(() { settings.useSystemTheme = v; }); updateTheme(); await settings.save(); }, ), leading: Icon(Icons.android) ), ListTile( title: Text('Player gradient background'.i18n), leading: Icon(Icons.colorize), trailing: Switch( value: settings.colorGradientBackground, onChanged: (bool v) async { setState(() => settings.colorGradientBackground = v); await settings.save(); }, ), ), ListTile( title: Text('Primary color'.i18n), leading: Icon(Icons.format_paint), subtitle: Text( 'Selected color'.i18n, style: TextStyle( color: settings.primaryColor ), ), onTap: () { showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Primary color'.i18n), content: Container( height: 240, child: MaterialColorPicker( colors: [ ...Colors.primaries, //Logo colors _swatch(0xffeca704), _swatch(0xffbe3266), _swatch(0xff4b2e7e), _swatch(0xff384697), _swatch(0xff0880b5), _swatch(0xff009a85), _swatch(0xff2ba766) ], allowShades: false, selectedColor: settings.primaryColor, onMainColorChange: (ColorSwatch color) { setState(() { settings.primaryColor = color; }); settings.save(); updateTheme(); Navigator.of(context).pop(); }, ), ), ); } ); }, ), ListTile( title: Text('Use album art primary color'.i18n), subtitle: Text('Warning: might be buggy'.i18n), leading: Icon(Icons.invert_colors), trailing: Switch( value: settings.useArtColor, onChanged: (v) => setState(() => settings.updateUseArtColor(v)), ), ) ], ), ); } } class QualitySettings extends StatefulWidget { @override _QualitySettingsState createState() => _QualitySettingsState(); } class _QualitySettingsState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Quality'.i18n), body: ListView( children: [ ListTile( title: Text('Mobile streaming'.i18n), leading: LeadingIcon(Icons.network_cell, color: Color(0xff384697)), ), QualityPicker('mobile'), FreezerDivider(), ListTile( title: Text('Wifi streaming'.i18n), leading: LeadingIcon(Icons.network_wifi, color: Color(0xff0880b5)), ), QualityPicker('wifi'), FreezerDivider(), ListTile( title: Text('Offline'.i18n), leading: LeadingIcon(Icons.offline_pin, color: Color(0xff009a85)), ), QualityPicker('offline'), FreezerDivider(), ListTile( title: Text('External downloads'.i18n), leading: LeadingIcon(Icons.file_download, color: Color(0xff2ba766)), ), QualityPicker('download'), ], ), ); } } class QualityPicker extends StatefulWidget { final String field; QualityPicker(this.field, {Key key}): super(key: key); @override _QualityPickerState createState() => _QualityPickerState(); } class _QualityPickerState extends State { AudioQuality _quality; @override void initState() { _getQuality(); super.initState(); } //Get current quality void _getQuality() { switch (widget.field) { case 'mobile': _quality = settings.mobileQuality; break; case 'wifi': _quality = settings.wifiQuality; break; case 'download': _quality = settings.downloadQuality; break; case 'offline': _quality = settings.offlineQuality; break; } } //Update quality in settings void _updateQuality(AudioQuality q) async { setState(() { _quality = q; }); switch (widget.field) { case 'mobile': settings.mobileQuality = _quality; settings.updateAudioServiceQuality(); break; case 'wifi': settings.wifiQuality = _quality; settings.updateAudioServiceQuality(); break; case 'download': settings.downloadQuality = _quality; break; case 'offline': settings.offlineQuality = _quality; break; } await settings.save(); await settings.updateAudioServiceQuality(); } @override Widget build(BuildContext context) { return Column( children: [ ListTile( title: Text('MP3 128kbps'), leading: Radio( groupValue: _quality, value: AudioQuality.MP3_128, onChanged: (q) => _updateQuality(q), ), ), ListTile( title: Text('MP3 320kbps'), leading: Radio( groupValue: _quality, value: AudioQuality.MP3_320, onChanged: (q) => _updateQuality(q), ), ), ListTile( title: Text('FLAC'), leading: Radio( groupValue: _quality, value: AudioQuality.FLAC, onChanged: (q) => _updateQuality(q), ), ), if (widget.field == 'download') ListTile( title: Text('Ask before downloading'), leading: Radio( groupValue: _quality, value: AudioQuality.ASK, onChanged: (q) => _updateQuality(q), ) ) ], ); } } class DeezerSettings extends StatefulWidget { @override _DeezerSettingsState createState() => _DeezerSettingsState(); } class _DeezerSettingsState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Deezer'.i18n), body: ListView( children: [ ListTile( title: Text('Content language'.i18n), subtitle: Text('Not app language, used in headers. Now'.i18n + ': ${settings.deezerLanguage}'), leading: Icon(Icons.language), onTap: () { showDialog( context: context, builder: (context) => LanguagePickerDialog( titlePadding: EdgeInsets.all(8.0), isSearchable: true, title: Text('Select language'.i18n), languagesList: defaultLanguagesList.map>((l) => { 'isoCode': l['isoCode'], 'name': l['name'] + ' (${l["isoCode"]})' }).toList(), onValuePicked: (Language language) { setState(() => settings.deezerLanguage = language.isoCode); settings.save(); }, ) ); }, ), ListTile( title: Text('Content country'.i18n), subtitle: Text('Country used in headers. Now'.i18n + ': ${settings.deezerCountry}'), leading: Icon(Icons.vpn_lock), onTap: () { showDialog( context: context, builder: (context) => CountryPickerDialog( titlePadding: EdgeInsets.all(8.0), isSearchable: true, onValuePicked: (Country country) { setState(() => settings.deezerCountry = country.isoCode); settings.save(); }, ) ); }, ), ListTile( title: Text('Log tracks'.i18n), subtitle: Text('Send track listen logs to Deezer, enable it for features like Flow to work properly'.i18n), trailing: Switch( value: settings.logListen, onChanged: (bool v) { setState(() => settings.logListen = v); settings.save(); }, ), leading: Icon(Icons.history_toggle_off), ), //TODO: Reimplement proxy // ListTile( // title: Text('Proxy'.i18n), // leading: Icon(Icons.vpn_key), // subtitle: Text(settings.proxyAddress??'Not set'.i18n), // onTap: () { // String _new; // showDialog( // context: context, // builder: (BuildContext context) { // return AlertDialog( // title: Text('Proxy'.i18n), // content: TextField( // onChanged: (String v) => _new = v, // decoration: InputDecoration( // hintText: 'IP:PORT' // ), // ), // actions: [ // FlatButton( // child: Text('Cancel'.i18n), // onPressed: () => Navigator.of(context).pop(), // ), // FlatButton( // child: Text('Reset'.i18n), // onPressed: () async { // setState(() { // settings.proxyAddress = null; // }); // await settings.save(); // Navigator.of(context).pop(); // }, // ), // FlatButton( // child: Text('Save'.i18n), // onPressed: () async { // setState(() { // settings.proxyAddress = _new; // }); // await settings.save(); // Navigator.of(context).pop(); // }, // ) // ], // ); // } // ); // }, // ) ], ), ); } } class DownloadsSettings extends StatefulWidget { @override _DownloadsSettingsState createState() => _DownloadsSettingsState(); } class _DownloadsSettingsState extends State { double _downloadThreads = settings.downloadThreads.toDouble(); @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('Download Settings'.i18n), body: ListView( children: [ ListTile( title: Text('Download path'.i18n), leading: Icon(Icons.folder), subtitle: Text(settings.downloadPath), onTap: () async { //Check permissions if (!(await Permission.storage.request().isGranted)) return; //Navigate Navigator.of(context).push(MaterialPageRoute( builder: (context) => DirectoryPicker(settings.downloadPath, onSelect: (String p) async { setState(() => settings.downloadPath = p); await settings.save(); },) )); }, ), ListTile( title: Text('Downloads naming'.i18n), subtitle: Text('Currently'.i18n + ': ${settings.downloadFilename}'), leading: Icon(Icons.text_format), onTap: () { showDialog( context: context, builder: (context) { TextEditingController _controller = TextEditingController(); String filename = settings.downloadFilename; _controller.value = _controller.value.copyWith(text: filename); String _new = _controller.value.text; //Dialog with filename format return AlertDialog( title: Text('Downloaded tracks filename'.i18n), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: _controller, onChanged: (String s) => _new = s, ), Container(height: 8.0), Text( 'Valid variables are'.i18n + ': %artists%, %artist%, %title%, %album%, %trackNumber%, %0trackNumber%, %feats%, %playlistTrackNumber%, %0playlistTrackNumber%, %year%, %date%\n\n' + "If you want to use custom directory naming - use '/' as directory separator.".i18n, style: TextStyle( fontSize: 12.0, ), ) ], ), actions: [ FlatButton( child: Text('Cancel'.i18n), onPressed: () => Navigator.of(context).pop(), ), FlatButton( child: Text('Reset'.i18n), onPressed: () { _controller.value = _controller.value.copyWith( text: '%artists% - %title%' ); _new = '%artists% - %title%'; }, ), FlatButton( child: Text('Clear'.i18n), onPressed: () => _controller.clear(), ), FlatButton( child: Text('Save'.i18n), onPressed: () async { setState(() { settings.downloadFilename = _new; }); await settings.save(); Navigator.of(context).pop(); }, ) ], ); } ); }, ), Padding( padding: EdgeInsets.symmetric(horizontal: 16.0), child: Text( 'Download threads'.i18n + ': ${_downloadThreads.round().toString()}', style: TextStyle( fontSize: 16.0 ), ), ), Slider( min: 1, max: 16, divisions: 15, value: _downloadThreads, label: _downloadThreads.round().toString(), onChanged: (double v) => setState(() => _downloadThreads = v), onChangeEnd: (double val) async { _downloadThreads = val; setState(() { settings.downloadThreads = _downloadThreads.round(); _downloadThreads = settings.downloadThreads.toDouble(); }); await settings.save(); //Prevent null if (val > 8 && cache.threadsWarning != true) { showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Warning'.i18n), content: Text('Using too many concurrent downloads on older/weaker devices might cause crashes!'.i18n), actions: [ FlatButton( child: Text('Dismiss'.i18n), onPressed: () => Navigator.of(context).pop(), ) ], ); } ); cache.threadsWarning = true; await cache.save(); } } ), FreezerDivider(), ListTile( title: Text('Create folders for artist'.i18n), trailing: Switch( value: settings.artistFolder, onChanged: (v) { setState(() => settings.artistFolder = v); settings.save(); }, ), leading: Icon(Icons.folder), ), ListTile( title: Text('Create folders for albums'.i18n), trailing: Switch( value: settings.albumFolder, onChanged: (v) { setState(() => settings.albumFolder = v); settings.save(); }, ), leading: Icon(Icons.folder) ), ListTile( title: Text('Create folder for playlist'.i18n), trailing: Switch( value: settings.playlistFolder, onChanged: (v) { setState(() => settings.playlistFolder = v); settings.save(); }, ), leading: Icon(Icons.folder) ), FreezerDivider(), ListTile( title: Text('Separate albums by discs'.i18n), trailing: Switch( value: settings.albumDiscFolder, onChanged: (v) { setState(() => settings.albumDiscFolder = v); settings.save(); }, ), leading: Icon(Icons.album) ), ListTile( title: Text('Overwrite already downloaded files'.i18n), trailing: Switch( value: settings.overwriteDownload, onChanged: (v) { setState(() => settings.overwriteDownload = v); settings.save(); }, ), leading: Icon(Icons.delete) ), ListTile( title: Text('Download .LRC lyrics'.i18n), trailing: Switch( value: settings.downloadLyrics, onChanged: (v) { setState(() => settings.downloadLyrics = v); settings.save(); }, ), leading: Icon(Icons.subtitles) ), FreezerDivider(), ListTile( title: Text('Save cover file for every track'.i18n), trailing: Switch( value: settings.trackCover, onChanged: (v) { setState(() => settings.trackCover = v); settings.save(); }, ), leading: Icon(Icons.image) ), ListTile( title: Text('Save album cover'.i18n), trailing: Switch( value: settings.albumCover, onChanged: (v) { setState(() => settings.albumCover = v); settings.save(); }, ), leading: Icon(Icons.image) ), ListTile( title: Text('Create .nomedia files'.i18n), subtitle: Text('To prevent gallery being filled with album art'.i18n), trailing: Switch( value: settings.nomediaFiles, onChanged: (v) { setState(() => settings.nomediaFiles = v); settings.save(); }, ), leading: Icon(Icons.insert_drive_file) ), FreezerDivider(), ListTile( title: Text('Download Log'.i18n), leading: Icon(Icons.sticky_note_2), onTap: () => Navigator.of(context).push( MaterialPageRoute(builder: (context) => DownloadLogViewer()) ), ) ], ), ); } } class GeneralSettings extends StatefulWidget { @override _GeneralSettingsState createState() => _GeneralSettingsState(); } class _GeneralSettingsState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('General'.i18n), body: ListView( children: [ ListTile( title: Text('Offline mode'.i18n), subtitle: Text('Will be overwritten on start.'.i18n), trailing: Switch( value: settings.offlineMode, onChanged: (bool v) { if (v) { setState(() => settings.offlineMode = true); return; } showDialog( context: context, builder: (context) { deezerAPI.authorize().then((v) { if (v) { setState(() => settings.offlineMode = false); } else { Fluttertoast.showToast( msg: 'Error logging in, check your internet connections.'.i18n, gravity: ToastGravity.BOTTOM, toastLength: Toast.LENGTH_SHORT ); } Navigator.of(context).pop(); }); return AlertDialog( title: Text('Logging in...'.i18n), content: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator() ], ) ); } ); }, ), leading: Icon(Icons.lock), ), ListTile( title: Text('Copy ARL'.i18n), subtitle: Text('Copy userToken/ARL Cookie for use in other apps.'.i18n), leading: Icon(Icons.lock), onTap: () async { await FlutterClipboard.copy(settings.arl); await Fluttertoast.showToast( msg: 'Copied'.i18n, ); }, ), ListTile( title: Text('LastFM'.i18n), subtitle: Text( (settings.lastFMPassword != null && settings.lastFMUsername != null) ? 'Log out'.i18n : 'Login to enable scrobbling.'.i18n ), leading: Icon(FontAwesome5.lastfm), onTap: () async { //Log out if (settings.lastFMPassword != null && settings.lastFMUsername != null) { settings.lastFMUsername = null; settings.lastFMPassword = null; playerHelper.scrobblenaut = null; await settings.save(); setState(() {}); Fluttertoast.showToast(msg: 'Logged out!'.i18n); return; } await showDialog( context: context, builder: (context) => LastFMLogin() ); setState(() {}); }, ), ListTile( title: Text('Log out'.i18n, style: TextStyle(color: Colors.red),), leading: Icon(Icons.exit_to_app), onTap: () { showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Log out'.i18n), content: Text('Due to plugin incompatibility, login using browser is unavailable without restart.'.i18n), actions: [ FlatButton( child: Text('Cancel'.i18n), onPressed: () => Navigator.of(context).pop(), ), FlatButton( child: Text('(ARL ONLY) Continue'.i18n), onPressed: () { logOut(); Navigator.of(context).pop(); }, ), FlatButton( child: Text('Log out & Exit'.i18n), onPressed: () async { try {AudioService.stop();} catch (e) {} await logOut(); SystemNavigator.pop(); }, ) ], ); } ); } ), ListTile( title: Text('Ignore interruptions'.i18n), subtitle: Text('Requires app restart to apply!'.i18n), leading: Icon(Icons.not_interested), trailing: Switch( value: settings.ignoreInterruptions, onChanged: (bool v) async { setState(() => settings.ignoreInterruptions = v); await settings.save(); }, ), ) ], ), ); } } class LastFMLogin extends StatefulWidget { @override _LastFMLoginState createState() => _LastFMLoginState(); } class _LastFMLoginState extends State { String _username = ''; String _password = ''; @override Widget build(BuildContext context) { return AlertDialog( title: Text('Login to LastFM'.i18n), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( decoration: InputDecoration( hintText: 'Username'.i18n ), onChanged: (v) => _username = v, ), Container(height: 8.0), TextField( obscureText: true, decoration: InputDecoration( hintText: 'Password'.i18n ), onChanged: (v) => _password = v, ) ], ), actions: [ FlatButton( child: Text('Cancel'.i18n), onPressed: () => Navigator.of(context).pop(), ), FlatButton( child: Text('Login'.i18n), onPressed: () async { LastFM last; try { last = await LastFM.authenticate( apiKey: 'b6ab5ae967bcd8b10b23f68f42493829', apiSecret: '861b0dff9a8a574bec747f9dab8b82bf', username: _username, password: _password ); } catch (e) { Fluttertoast.showToast(msg: 'Authorization error!'.i18n); return; } //Save settings.lastFMUsername = last.username; settings.lastFMPassword = last.passwordHash; await settings.save(); playerHelper.scrobblenaut = Scrobblenaut(lastFM: last); Navigator.of(context).pop(); }, ), ], ); } } class DirectoryPicker extends StatefulWidget { final String initialPath; final Function onSelect; DirectoryPicker(this.initialPath, {this.onSelect, Key key}): super(key: key); @override _DirectoryPickerState createState() => _DirectoryPickerState(); } class _DirectoryPickerState extends State { String _path; String _previous; String _root; @override void initState() { _path = widget.initialPath; super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar( 'Pick-a-Path'.i18n, actions: [ IconButton( icon: Icon(Icons.sd_card), onPressed: () { String path = ''; //Chose storage showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Select storage'.i18n), content: FutureBuilder( future: PathProviderEx.getStorageInfo(), builder: (context, snapshot) { if (snapshot.hasError) return ErrorScreen(); if (!snapshot.hasData) return Padding( padding: EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator() ], ), ); return Column( mainAxisSize: MainAxisSize.min, children: [ ...List.generate(snapshot.data.length, (i) { StorageInfo si = snapshot.data[i]; return ListTile( title: Text(si.rootDir), leading: Icon(Icons.sd_card), trailing: Text(filesize(si.availableBytes)), onTap: () { setState(() { _path = si.appFilesDir; //Android 5+ blocks sd card, so this prevents going outside //app data dir, until permission request fix. _root = si.rootDir; if (i != 0) _root = si.appFilesDir; }); Navigator.of(context).pop(); }, ); }) ], ); }, ), ); } ); } ) ], ), floatingActionButton: FloatingActionButton( child: Icon(Icons.done), onPressed: () { //When folder confirmed if (widget.onSelect != null) widget.onSelect(_path); Navigator.of(context).pop(); }, ), body: FutureBuilder( future: Directory(_path).list().toList(), builder: (BuildContext context, AsyncSnapshot snapshot) { //On error go to last good path if (snapshot.hasError) Future.delayed(Duration(milliseconds: 50), () => setState(() => _path = _previous)); if (!snapshot.hasData) return Center(child: CircularProgressIndicator(),); List data = snapshot.data; return ListView( children: [ ListTile( title: Text(_path), ), ListTile( title: Text('Go up'.i18n), leading: Icon(Icons.arrow_upward), onTap: () { setState(() { if (_root == _path) { Fluttertoast.showToast( msg: 'Permission denied'.i18n, gravity: ToastGravity.BOTTOM ); return; } _previous = _path; _path = Directory(_path).parent.path; }); }, ), ...List.generate(data.length, (i) { FileSystemEntity f = data[i]; if (f is Directory) { return ListTile( title: Text(f.path.split('/').last), leading: Icon(Icons.folder), onTap: () { setState(() { _previous = _path; _path = f.path; }); }, ); } return Container(height: 0, width: 0,); }) ], ); }, ), ); } } class CreditsScreen extends StatefulWidget { @override _CreditsScreenState createState() => _CreditsScreenState(); } class _CreditsScreenState extends State { String _version = ''; static final List> translators = [ ['Xandar Null', 'Arabic'], ['Markus', 'German'], ['Andrea', 'Italian'], ['Diego Hiro', 'Portuguese'], ['Orfej', 'Russian'], ['Chino Pacia', 'Filipino'], ['ArcherDelta & PetFix', 'Spanish'], ['Shazzaam', 'Croatian'], ['VIRGIN_KLM', 'Greek'], ['koreezzz', 'Korean'], ['Fwwwwwwwwwweze', 'French'], ['kobyrevah', 'Hebrew'], ['HoScHaKaL', 'Turkish'], ['MicroMihai', 'Romanian'], ['LenteraMalam', 'Indonesian'], ['RTWO2', 'Persian'] ]; @override void initState() { PackageInfo.fromPlatform().then((info) { setState(() { _version = 'v${info.version}'; }); }); super.initState(); } @override Widget build(BuildContext context) { return Scaffold( appBar: FreezerAppBar('About'.i18n), body: ListView( children: [ FreezerTitle(), Text( _version, textAlign: TextAlign.center, style: TextStyle( fontStyle: FontStyle.italic ), ), FreezerDivider(), ListTile( title: Text('Telegram Channel'.i18n), subtitle: Text('To get latest releases'.i18n), leading: Icon(FontAwesome5.telegram, color: Color(0xFF27A2DF), size: 36.0), onTap: () { launch('https://t.me/freezereleases'); }, ), ListTile( title: Text('Telegram Group'.i18n), subtitle: Text('Official chat'.i18n), leading: Icon(FontAwesome5.telegram, color: Colors.cyan, size: 36.0), onTap: () { launch('https://t.me/freezerandroid'); }, ), ListTile( title: Text('Repository'.i18n), subtitle: Text('Source code, report issues there.'.i18n), leading: Icon(Icons.code, color: Colors.green, size: 36.0), onTap: () { launch('https://notabug.org/exttex/freezer'); }, ), FreezerDivider(), ListTile( title: Text('exttex'), subtitle: Text('Developer'), ), ListTile( title: Text('Bas Curtiz'), subtitle: Text('Icon, logo, banner, design suggestions, tester'), ), ListTile( title: Text('Tobs'), subtitle: Text('Alpha testers'), ), ListTile( title: Text('Deemix'), subtitle: Text('Better app <3'), ), ListTile( title: Text('Xandar Null'), subtitle: Text('Tester, translations help'), ), ListTile( title: Text('Francesco'), subtitle: Text('Tester'), onTap: () { setState(() { settings.primaryColor = Color(0xff333333); }); updateTheme(); settings.save(); }, ), ListTile( title: Text('Annexhack'), subtitle: Text('Android Auto help'), ), FreezerDivider(), ...List.generate(translators.length, (i) => ListTile( title: Text(translators[i][0]), subtitle: Text(translators[i][1]), )), Padding( padding: EdgeInsets.fromLTRB(0, 4, 0, 8), child: Text( 'Huge thanks to all the contributors! <3'.i18n, textAlign: TextAlign.center, style: TextStyle( fontSize: 16.0 ), ), ) ], ), ); } }