diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index 058c4d4..3546fb8 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -677,5 +677,6 @@ "publisherFeatureDevelopHint": "Currently, this feature is under active development, you need send a request to unlock this feature.", "learnMore": "Learn More", "discoverWebArticles": "Articles from external sites", - "webArticlesStand": "Article Stand" + "webArticlesStand": "Article Stand", + "about": "About" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 92f5fbe..d55e7ed 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -80,6 +80,8 @@ PODS: - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 6.0.3) + - flutter_keyboard_visibility (0.0.1): + - Flutter - flutter_native_splash (2.4.3): - Flutter - flutter_platform_alert (0.0.1): @@ -155,6 +157,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - pointer_interceptor_ios (0.0.1): + - Flutter - PromisesObjC (2.4.0) - receive_sharing_intent (1.8.1): - Flutter @@ -217,6 +221,7 @@ DEPENDENCIES: - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_platform_alert (from `.symlinks/plugins/flutter_platform_alert/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) @@ -235,6 +240,7 @@ DEPENDENCIES: - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - pasteboard (from `.symlinks/plugins/pasteboard/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - pointer_interceptor_ios (from `.symlinks/plugins/pointer_interceptor_ios/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - record_ios (from `.symlinks/plugins/record_ios/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) @@ -286,6 +292,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_keyboard_visibility: + :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_platform_alert: @@ -320,6 +328,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/pasteboard/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + pointer_interceptor_ios: + :path: ".symlinks/plugins/pointer_interceptor_ios/ios" receive_sharing_intent: :path: ".symlinks/plugins/receive_sharing_intent/ios" record_ios: @@ -360,6 +370,7 @@ SPEC CHECKSUMS: FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619 flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3 flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 @@ -382,6 +393,7 @@ SPEC CHECKSUMS: package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b diff --git a/lib/models/auto_completion.dart b/lib/models/auto_completion.dart new file mode 100644 index 0000000..8f1ac1a --- /dev/null +++ b/lib/models/auto_completion.dart @@ -0,0 +1,34 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'auto_completion.freezed.dart'; +part 'auto_completion.g.dart'; + +@freezed +sealed class AutoCompletionResponse with _$AutoCompletionResponse { + const factory AutoCompletionResponse.account({ + required String type, + required List items, + }) = AutoCompletionAccountResponse; + + const factory AutoCompletionResponse.sticker({ + required String type, + required List items, + }) = AutoCompletionStickerResponse; + + factory AutoCompletionResponse.fromJson(Map json) => + _$AutoCompletionResponseFromJson(json); +} + +@freezed +sealed class AutoCompletionItem with _$AutoCompletionItem { + const factory AutoCompletionItem({ + required String id, + required String displayName, + required String? secondaryText, + required String type, + required dynamic data, + }) = _AutoCompletionItem; + + factory AutoCompletionItem.fromJson(Map json) => + _$AutoCompletionItemFromJson(json); +} diff --git a/lib/models/auto_completion.freezed.dart b/lib/models/auto_completion.freezed.dart new file mode 100644 index 0000000..74575bc --- /dev/null +++ b/lib/models/auto_completion.freezed.dart @@ -0,0 +1,410 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'auto_completion.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +AutoCompletionResponse _$AutoCompletionResponseFromJson( + Map json +) { + switch (json['runtimeType']) { + case 'account': + return AutoCompletionAccountResponse.fromJson( + json + ); + case 'sticker': + return AutoCompletionStickerResponse.fromJson( + json + ); + + default: + throw CheckedFromJsonException( + json, + 'runtimeType', + 'AutoCompletionResponse', + 'Invalid union type "${json['runtimeType']}"!' +); + } + +} + +/// @nodoc +mixin _$AutoCompletionResponse { + + String get type; List get items; +/// Create a copy of AutoCompletionResponse +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AutoCompletionResponseCopyWith get copyWith => _$AutoCompletionResponseCopyWithImpl(this as AutoCompletionResponse, _$identity); + + /// Serializes this AutoCompletionResponse to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.items, items)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(items)); + +@override +String toString() { + return 'AutoCompletionResponse(type: $type, items: $items)'; +} + + +} + +/// @nodoc +abstract mixin class $AutoCompletionResponseCopyWith<$Res> { + factory $AutoCompletionResponseCopyWith(AutoCompletionResponse value, $Res Function(AutoCompletionResponse) _then) = _$AutoCompletionResponseCopyWithImpl; +@useResult +$Res call({ + String type, List items +}); + + + + +} +/// @nodoc +class _$AutoCompletionResponseCopyWithImpl<$Res> + implements $AutoCompletionResponseCopyWith<$Res> { + _$AutoCompletionResponseCopyWithImpl(this._self, this._then); + + final AutoCompletionResponse _self; + final $Res Function(AutoCompletionResponse) _then; + +/// Create a copy of AutoCompletionResponse +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? items = null,}) { + return _then(_self.copyWith( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// @nodoc +@JsonSerializable() + +class AutoCompletionAccountResponse implements AutoCompletionResponse { + const AutoCompletionAccountResponse({required this.type, required final List items, final String? $type}): _items = items,$type = $type ?? 'account'; + factory AutoCompletionAccountResponse.fromJson(Map json) => _$AutoCompletionAccountResponseFromJson(json); + +@override final String type; + final List _items; +@override List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); +} + + +@JsonKey(name: 'runtimeType') +final String $type; + + +/// Create a copy of AutoCompletionResponse +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AutoCompletionAccountResponseCopyWith get copyWith => _$AutoCompletionAccountResponseCopyWithImpl(this, _$identity); + +@override +Map toJson() { + return _$AutoCompletionAccountResponseToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionAccountResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._items, _items)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_items)); + +@override +String toString() { + return 'AutoCompletionResponse.account(type: $type, items: $items)'; +} + + +} + +/// @nodoc +abstract mixin class $AutoCompletionAccountResponseCopyWith<$Res> implements $AutoCompletionResponseCopyWith<$Res> { + factory $AutoCompletionAccountResponseCopyWith(AutoCompletionAccountResponse value, $Res Function(AutoCompletionAccountResponse) _then) = _$AutoCompletionAccountResponseCopyWithImpl; +@override @useResult +$Res call({ + String type, List items +}); + + + + +} +/// @nodoc +class _$AutoCompletionAccountResponseCopyWithImpl<$Res> + implements $AutoCompletionAccountResponseCopyWith<$Res> { + _$AutoCompletionAccountResponseCopyWithImpl(this._self, this._then); + + final AutoCompletionAccountResponse _self; + final $Res Function(AutoCompletionAccountResponse) _then; + +/// Create a copy of AutoCompletionResponse +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? items = null,}) { + return _then(AutoCompletionAccountResponse( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + +/// @nodoc +@JsonSerializable() + +class AutoCompletionStickerResponse implements AutoCompletionResponse { + const AutoCompletionStickerResponse({required this.type, required final List items, final String? $type}): _items = items,$type = $type ?? 'sticker'; + factory AutoCompletionStickerResponse.fromJson(Map json) => _$AutoCompletionStickerResponseFromJson(json); + +@override final String type; + final List _items; +@override List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); +} + + +@JsonKey(name: 'runtimeType') +final String $type; + + +/// Create a copy of AutoCompletionResponse +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AutoCompletionStickerResponseCopyWith get copyWith => _$AutoCompletionStickerResponseCopyWithImpl(this, _$identity); + +@override +Map toJson() { + return _$AutoCompletionStickerResponseToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionStickerResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._items, _items)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_items)); + +@override +String toString() { + return 'AutoCompletionResponse.sticker(type: $type, items: $items)'; +} + + +} + +/// @nodoc +abstract mixin class $AutoCompletionStickerResponseCopyWith<$Res> implements $AutoCompletionResponseCopyWith<$Res> { + factory $AutoCompletionStickerResponseCopyWith(AutoCompletionStickerResponse value, $Res Function(AutoCompletionStickerResponse) _then) = _$AutoCompletionStickerResponseCopyWithImpl; +@override @useResult +$Res call({ + String type, List items +}); + + + + +} +/// @nodoc +class _$AutoCompletionStickerResponseCopyWithImpl<$Res> + implements $AutoCompletionStickerResponseCopyWith<$Res> { + _$AutoCompletionStickerResponseCopyWithImpl(this._self, this._then); + + final AutoCompletionStickerResponse _self; + final $Res Function(AutoCompletionStickerResponse) _then; + +/// Create a copy of AutoCompletionResponse +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? items = null,}) { + return _then(AutoCompletionStickerResponse( +type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + + +/// @nodoc +mixin _$AutoCompletionItem { + + String get id; String get displayName; String? get secondaryText; String get type; dynamic get data; +/// Create a copy of AutoCompletionItem +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AutoCompletionItemCopyWith get copyWith => _$AutoCompletionItemCopyWithImpl(this as AutoCompletionItem, _$identity); + + /// Serializes this AutoCompletionItem to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionItem&&(identical(other.id, id) || other.id == id)&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.secondaryText, secondaryText) || other.secondaryText == secondaryText)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,displayName,secondaryText,type,const DeepCollectionEquality().hash(data)); + +@override +String toString() { + return 'AutoCompletionItem(id: $id, displayName: $displayName, secondaryText: $secondaryText, type: $type, data: $data)'; +} + + +} + +/// @nodoc +abstract mixin class $AutoCompletionItemCopyWith<$Res> { + factory $AutoCompletionItemCopyWith(AutoCompletionItem value, $Res Function(AutoCompletionItem) _then) = _$AutoCompletionItemCopyWithImpl; +@useResult +$Res call({ + String id, String displayName, String? secondaryText, String type, dynamic data +}); + + + + +} +/// @nodoc +class _$AutoCompletionItemCopyWithImpl<$Res> + implements $AutoCompletionItemCopyWith<$Res> { + _$AutoCompletionItemCopyWithImpl(this._self, this._then); + + final AutoCompletionItem _self; + final $Res Function(AutoCompletionItem) _then; + +/// Create a copy of AutoCompletionItem +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? displayName = null,Object? secondaryText = freezed,Object? type = null,Object? data = freezed,}) { + return _then(_self.copyWith( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable +as String,secondaryText: freezed == secondaryText ? _self.secondaryText : secondaryText // ignore: cast_nullable_to_non_nullable +as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as dynamic, + )); +} + +} + + +/// @nodoc +@JsonSerializable() + +class _AutoCompletionItem implements AutoCompletionItem { + const _AutoCompletionItem({required this.id, required this.displayName, required this.secondaryText, required this.type, required this.data}); + factory _AutoCompletionItem.fromJson(Map json) => _$AutoCompletionItemFromJson(json); + +@override final String id; +@override final String displayName; +@override final String? secondaryText; +@override final String type; +@override final dynamic data; + +/// Create a copy of AutoCompletionItem +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AutoCompletionItemCopyWith<_AutoCompletionItem> get copyWith => __$AutoCompletionItemCopyWithImpl<_AutoCompletionItem>(this, _$identity); + +@override +Map toJson() { + return _$AutoCompletionItemToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutoCompletionItem&&(identical(other.id, id) || other.id == id)&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.secondaryText, secondaryText) || other.secondaryText == secondaryText)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,id,displayName,secondaryText,type,const DeepCollectionEquality().hash(data)); + +@override +String toString() { + return 'AutoCompletionItem(id: $id, displayName: $displayName, secondaryText: $secondaryText, type: $type, data: $data)'; +} + + +} + +/// @nodoc +abstract mixin class _$AutoCompletionItemCopyWith<$Res> implements $AutoCompletionItemCopyWith<$Res> { + factory _$AutoCompletionItemCopyWith(_AutoCompletionItem value, $Res Function(_AutoCompletionItem) _then) = __$AutoCompletionItemCopyWithImpl; +@override @useResult +$Res call({ + String id, String displayName, String? secondaryText, String type, dynamic data +}); + + + + +} +/// @nodoc +class __$AutoCompletionItemCopyWithImpl<$Res> + implements _$AutoCompletionItemCopyWith<$Res> { + __$AutoCompletionItemCopyWithImpl(this._self, this._then); + + final _AutoCompletionItem _self; + final $Res Function(_AutoCompletionItem) _then; + +/// Create a copy of AutoCompletionItem +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? displayName = null,Object? secondaryText = freezed,Object? type = null,Object? data = freezed,}) { + return _then(_AutoCompletionItem( +id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable +as String,secondaryText: freezed == secondaryText ? _self.secondaryText : secondaryText // ignore: cast_nullable_to_non_nullable +as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable +as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable +as dynamic, + )); +} + + +} + +// dart format on diff --git a/lib/models/auto_completion.g.dart b/lib/models/auto_completion.g.dart new file mode 100644 index 0000000..a0b7d17 --- /dev/null +++ b/lib/models/auto_completion.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'auto_completion.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AutoCompletionAccountResponse _$AutoCompletionAccountResponseFromJson( + Map json, +) => AutoCompletionAccountResponse( + type: json['type'] as String, + items: + (json['items'] as List) + .map((e) => AutoCompletionItem.fromJson(e as Map)) + .toList(), + $type: json['runtimeType'] as String?, +); + +Map _$AutoCompletionAccountResponseToJson( + AutoCompletionAccountResponse instance, +) => { + 'type': instance.type, + 'items': instance.items.map((e) => e.toJson()).toList(), + 'runtimeType': instance.$type, +}; + +AutoCompletionStickerResponse _$AutoCompletionStickerResponseFromJson( + Map json, +) => AutoCompletionStickerResponse( + type: json['type'] as String, + items: + (json['items'] as List) + .map((e) => AutoCompletionItem.fromJson(e as Map)) + .toList(), + $type: json['runtimeType'] as String?, +); + +Map _$AutoCompletionStickerResponseToJson( + AutoCompletionStickerResponse instance, +) => { + 'type': instance.type, + 'items': instance.items.map((e) => e.toJson()).toList(), + 'runtimeType': instance.$type, +}; + +_AutoCompletionItem _$AutoCompletionItemFromJson(Map json) => + _AutoCompletionItem( + id: json['id'] as String, + displayName: json['display_name'] as String, + secondaryText: json['secondary_text'] as String?, + type: json['type'] as String, + data: json['data'], + ); + +Map _$AutoCompletionItemToJson(_AutoCompletionItem instance) => + { + 'id': instance.id, + 'display_name': instance.displayName, + 'secondary_text': instance.secondaryText, + 'type': instance.type, + 'data': instance.data, + }; diff --git a/lib/route.dart b/lib/route.dart index 203e478..82d2fa1 100644 --- a/lib/route.dart +++ b/lib/route.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/screens/about.dart'; import 'package:island/screens/developers/apps.dart'; import 'package:island/screens/developers/edit_app.dart'; import 'package:island/screens/developers/new_app.dart'; @@ -249,6 +250,10 @@ final routerProvider = Provider((ref) { path: '/settings', builder: (context, state) => const SettingsScreen(), ), + GoRoute( + path: '/about', + builder: (context, state) => const AboutScreen(), + ), // Main tabs with TabsScreen shell ShellRoute( diff --git a/lib/screens/about.dart b/lib/screens/about.dart new file mode 100644 index 0000000..1b1c625 --- /dev/null +++ b/lib/screens/about.dart @@ -0,0 +1,300 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AboutScreen extends StatefulWidget { + const AboutScreen({super.key}); + + @override + State createState() => _AboutScreenState(); +} + +class _AboutScreenState extends State { + PackageInfo _packageInfo = PackageInfo( + appName: 'Island', + packageName: 'com.example.island', + version: '1.0.0', + buildNumber: '1', + ); + bool _isLoading = true; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _initPackageInfo(); + } + + Future _initPackageInfo() async { + try { + final info = await PackageInfo.fromPlatform(); + if (mounted) { + setState(() { + _packageInfo = info; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _errorMessage = 'Failed to load package info: $e'; + _isLoading = false; + }); + } + } + } + + Future _launchURL(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri); + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Scaffold( + appBar: AppBar(title: const Text('About'), elevation: 0), + body: + _isLoading + ? const Center(child: CircularProgressIndicator()) + : _errorMessage != null + ? Center(child: Text(_errorMessage!)) + : SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 24), + // App Icon and Name + CircleAvatar( + radius: 50, + backgroundColor: theme.colorScheme.primary.withOpacity( + 0.1, + ), + child: Image.asset( + 'assets/icons/icon.png', + width: 56, + height: 56, + ), + ), + const SizedBox(height: 16), + Text( + _packageInfo.appName, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text( + 'Version ${_packageInfo.version} (${_packageInfo.buildNumber})', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + ), + const SizedBox(height: 32), + + // App Info Card + _buildSection( + context, + title: 'App Information', + children: [ + _buildInfoItem( + context, + icon: Icons.info_outline, + label: 'Package Name', + value: _packageInfo.packageName, + ), + _buildInfoItem( + context, + icon: Icons.update, + label: 'Version', + value: _packageInfo.version, + ), + _buildInfoItem( + context, + icon: Icons.build, + label: 'Build Number', + value: _packageInfo.buildNumber, + ), + ], + ), + + const SizedBox(height: 16), + + // Links Card + _buildSection( + context, + title: 'Links', + children: [ + _buildListTile( + context, + icon: Icons.privacy_tip_outlined, + title: 'Privacy Policy', + onTap: + () => _launchURL( + 'https://solsynth.dev/terms/privacy-policy', + ), + ), + _buildListTile( + context, + icon: Icons.description_outlined, + title: 'Terms of Service', + onTap: + () => _launchURL( + 'https://example.com/terms/basic-law', + ), + ), + _buildListTile( + context, + icon: Icons.code, + title: 'Open Source Licenses', + onTap: () { + showLicensePage( + context: context, + applicationName: _packageInfo.appName, + applicationVersion: + 'Version ${_packageInfo.version}', + ); + }, + ), + ], + ), + + const SizedBox(height: 16), + + // Developer Info + _buildSection( + context, + title: 'Developer', + children: [ + _buildListTile( + context, + icon: Icons.email_outlined, + title: 'Contact Us', + subtitle: 'lily@solsynth.dev', + onTap: () => _launchURL('mailto:lily@solsynth.dev'), + ), + _buildListTile( + context, + icon: Icons.copyright, + title: 'License', + subtitle: + 'Copyright reserved © ${DateTime.now().year} Solsynth\nGNU Affero General Public License v3.0', + onTap: + () => _launchURL( + 'https://github.com/Solsynth/Solian/blob/v3/LICENSE.txt', + ), + ), + ], + ), + + const SizedBox(height: 32), + + // Copyright + Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + '© ${DateTime.now().year} ${_packageInfo.appName}. All rights reserved.', + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } + + Widget _buildSection( + BuildContext context, { + required String title, + required List children, + }) { + return Card( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + title, + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + const Divider(height: 1), + ...children, + ], + ), + ); + } + + Widget _buildInfoItem( + BuildContext context, { + required IconData icon, + required String label, + required String value, + }) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + Icon(icon, size: 20, color: Theme.of(context).hintColor), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: Theme.of(context).textTheme.bodySmall), + const SizedBox(height: 2), + SelectableText( + value, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + if (value.startsWith('http') || value.contains('@')) + IconButton( + icon: const Icon(Icons.copy, size: 16), + onPressed: () { + Clipboard.setData(ClipboardData(text: value)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Copied to clipboard')), + ); + }, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + tooltip: 'Copy to clipboard', + ), + ], + ), + ); + } + + Widget _buildListTile( + BuildContext context, { + required IconData icon, + required String title, + String? subtitle, + required VoidCallback onTap, + }) { + return Column( + children: [ + ListTile( + leading: Icon(icon), + title: Text(title), + subtitle: subtitle != null ? Text(subtitle) : null, + trailing: const Icon(Icons.chevron_right), + onTap: onTap, + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + minLeadingWidth: 24, + ), + ], + ); + } +} diff --git a/lib/screens/account.dart b/lib/screens/account.dart index f043341..5d08c10 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -281,6 +281,16 @@ class AccountScreen extends HookConsumerWidget { }, ), const Divider(height: 1).padding(vertical: 8), + ListTile( + minTileHeight: 48, + leading: const Icon(Symbols.info), + trailing: const Icon(Symbols.chevron_right), + contentPadding: EdgeInsets.symmetric(horizontal: 24), + title: Text('about').tr(), + onTap: () { + context.push('/about'); + }, + ), ListTile( minTileHeight: 48, leading: const Icon(Symbols.logout), diff --git a/lib/widgets/web_article_card.dart b/lib/widgets/web_article_card.dart index 427045a..221cd21 100644 --- a/lib/widgets/web_article_card.dart +++ b/lib/widgets/web_article_card.dart @@ -81,7 +81,10 @@ class WebArticleCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ - if (showDetails) const SizedBox(height: 8), + if (showDetails) + const SizedBox(height: 8) + else + Spacer(), Text( article.title, style: theme.textTheme.titleSmall?.copyWith( @@ -104,7 +107,7 @@ class WebArticleCard extends StatelessWidget { ), ), ], - const Spacer(), + if (showDetails) const Spacer(), if (showDetails && article.publishedAt != null) ...[ Text( '${article.publishedAt!.formatSystem()} · ${article.publishedAt!.formatRelative(context)}', diff --git a/pubspec.lock b/pubspec.lock index 6a8456e..c861c92 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -798,6 +798,54 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.0" + flutter_keyboard_visibility: + dependency: transitive + description: + name: flutter_keyboard_visibility + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_platform_interface: + dependency: transitive + description: + name: flutter_keyboard_visibility_platform_interface + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_web: + dependency: transitive + description: + name: flutter_keyboard_visibility_web + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -960,6 +1008,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.1" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + sha256: d64712c65db240b1057559b952398ebb6e498077baeebf9b0731dade62438a6d + url: "https://pub.dev" + source: hosted + version: "5.2.0" flutter_udid: dependency: "direct main" description: @@ -1685,6 +1741,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + pointer_interceptor: + dependency: transitive + description: + name: pointer_interceptor + sha256: "57210410680379aea8b1b7ed6ae0c3ad349bfd56fe845b8ea934a53344b9d523" + url: "https://pub.dev" + source: hosted + version: "0.10.1+2" + pointer_interceptor_ios: + dependency: transitive + description: + name: pointer_interceptor_ios + sha256: a6906772b3205b42c44614fcea28f818b1e5fdad73a4ca742a7bd49818d9c917 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + pointer_interceptor_platform_interface: + dependency: transitive + description: + name: pointer_interceptor_platform_interface + sha256: "0597b0560e14354baeb23f8375cd612e8bd4841bf8306ecb71fcd0bb78552506" + url: "https://pub.dev" + source: hosted + version: "0.10.0+1" + pointer_interceptor_web: + dependency: transitive + description: + name: pointer_interceptor_web + sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a" + url: "https://pub.dev" + source: hosted + version: "0.10.3" pool: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c0dbe4a..ad2a3eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 3.0.0+109 +version: 3.0.0+110 environment: sdk: ^3.7.2 @@ -128,6 +128,7 @@ dependencies: ref: fixes/allow-controller-re-registration mime: ^2.0.0 html2md: ^1.3.2 + flutter_typeahead: ^5.2.0 dev_dependencies: flutter_test: