Compare commits

...

25 Commits

Author SHA1 Message Date
925cb2b423 🔀 Merge pull request #51 from Solsynth/l10n_v3
New Crowdin updates
2025-07-02 21:36:36 +08:00
0a2804a404 New translations assets/i18n/en-US.json (bundle: 6) 2025-07-02 21:34:38 +08:00
12bbcbf69c New translations assets/i18n/zh-TW.json (bundle: 6) 2025-07-02 21:34:37 +08:00
52ce490725 New translations assets/i18n/zh-CN.json (bundle: 6) 2025-07-02 21:34:37 +08:00
82067fb3aa 🐛 Fix explore page styling 2025-07-02 13:17:45 +08:00
007acedf29 🚀 Launch 3.0.0+110 2025-07-02 02:23:46 +08:00
8e903ec6c1 About page 2025-07-02 02:20:31 +08:00
b55e56c3c4 Web articles list 2025-07-02 01:11:25 +08:00
6f9de431b1 Search post 2025-07-02 00:40:35 +08:00
a8efd26262 ⬆️ Upgrade dependencies 2025-07-01 22:54:58 +08:00
e367fc3f5c Web articles detail page & explore feed 2025-07-01 13:19:14 +08:00
8a1af120ea Discover article 2025-07-01 01:34:34 +08:00
f03f0181f8 EXIF viewer 2025-07-01 00:14:12 +08:00
6c7d42c31a 🐛 Bug fixes 2025-06-30 23:55:44 +08:00
d6c829c26a Web feed 2025-06-30 23:33:14 +08:00
666a2dfbf5 Feature flags 2025-06-30 01:11:31 +08:00
fd979c3a35 Custom apps 2025-06-30 00:22:59 +08:00
847fc6e864 💄 Optimized custom app display 2025-06-29 23:34:56 +08:00
356b7bf01a Developer app basis 2025-06-29 23:00:51 +08:00
450d5ebc81 Developer portal basis 2025-06-29 19:36:37 +08:00
f04285848f 🐛 Fix upload file in share sheet 2025-06-29 18:03:18 +08:00
c4becb0a05 Publisher members 2025-06-28 18:57:32 +08:00
d22619396b 🐛 Fix linux builds 2025-06-28 11:45:50 +08:00
fe8640a6db 🚀 Launch 3.0.0+109 2025-06-28 02:48:15 +08:00
ff475d43dd 🐛 Dozen of hot fixes 2025-06-28 02:40:50 +08:00
88 changed files with 20398 additions and 2029 deletions

View File

@ -3,7 +3,7 @@ name: Build Release
on:
push:
tags:
- '*'
- "*"
workflow_dispatch:
jobs:
@ -59,6 +59,7 @@ jobs:
sudo apt-get install -y libnotify-dev
sudo apt-get install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
sudo apt-get install -y gstreamer-1.0
sudo apt-get install -y libsecret-1-0
- run: flutter pub get
- run: flutter build linux
- name: Archive production artifacts
@ -80,4 +81,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: build-output-linux-appimage
path: './*.AppImage*'
path: "./*.AppImage*"

View File

@ -98,7 +98,7 @@
<receiver
android:name=".receiver.ReplyReceiver"
android:enabled="true"
android:exported="false" />
android:exported="true" />
<service
android:name=".service.MessagingService"

View File

@ -1,14 +1,39 @@
package dev.solsynth.solian
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin
class MainActivity : FlutterActivity()
{
private val CHANNEL = "dev.solsynth.solian/notifications"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// https://github.com/flutter/flutter/issues/153075#issuecomment-2693189362
flutterEngine.plugins.add(LegacySharedPreferencesPlugin())
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "initialLink") {
val roomId = intent.getStringExtra("room_id")
if (roomId != null) {
result.success("/rooms/$roomId")
} else {
result.success(null)
}
} else {
result.notImplemented()
}
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val roomId = intent.getStringExtra("room_id")
if (roomId != null) {
MethodChannel(flutterEngine!!.dartExecutor.binaryMessenger, CHANNEL).invokeMethod("newLink", "/rooms/$roomId")
}
}
}

View File

@ -1,48 +1,47 @@
package dev.solsynth.solian.network
import android.content.Context
import okhttp3.*
import android.content.SharedPreferences
import okhttp3.Call
import okhttp3.Callback
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject
import java.io.IOException
class ApiClient(private val context: Context) {
private val client = OkHttpClient()
private val sharedPreferences: SharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
fun sendMessage(roomId: String, content: String, repliedMessageId: String, callback: () -> Unit) {
val prefs = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
val token = prefs.getString("flutter.token", null)
val serverUrl = prefs.getString("flutter.serverUrl", null)
if (token == null || serverUrl == null) {
fun sendMessage(roomId: String, message: String, replyTo: String, callback: (Boolean) -> Unit) {
val token = sharedPreferences.getString("flutter.token", null)
if (token == null) {
callback(false)
return
}
val url = "$serverUrl/chat/$roomId/messages"
val json = JSONObject()
json.put("content", content)
json.put("replied_message_id", repliedMessageId)
val requestBody = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val json = JSONObject().apply {
put("content", message)
put("reply_to", replyTo)
}
val body = json.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
val request = Request.Builder()
.url(url)
.post(requestBody)
.addHeader("Authorization", "AtField $token")
.url("https://solian.dev/api/rooms/$roomId/messages")
.header("Authorization", "Bearer $token")
.post(body)
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
// Handle failure
callback()
callback(false)
}
override fun onResponse(call: Call, response: Response) {
// Handle success
callback()
callback(response.isSuccessful)
}
})
}
}
}

View File

@ -72,6 +72,7 @@ class MessagingService: FirebaseMessagingService() {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
intent.putExtra("room_id", roomId)
val pendingIntent = PendingIntent.getActivity(this, 0, intent, pendingIntentFlags)
val notificationBuilder = NotificationCompat.Builder(this, "messages")

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -40,31 +40,31 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (11.13.0):
- FirebaseCore (~> 11.13.0)
- Firebase/Messaging (11.13.0):
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.13.0)
- firebase_core (3.14.0):
- Firebase/CoreOnly (= 11.13.0)
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.0):
- Firebase/CoreOnly (= 11.15.0)
- Flutter
- firebase_messaging (15.2.7):
- Firebase/Messaging (= 11.13.0)
- firebase_messaging (15.2.8):
- Firebase/Messaging (= 11.15.0)
- firebase_core
- Flutter
- FirebaseCore (11.13.0):
- FirebaseCoreInternal (~> 11.13.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.13.0):
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
@ -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):
@ -128,8 +130,8 @@ PODS:
- Flutter
- irondash_engine_context (0.0.1):
- Flutter
- Kingfisher (8.3.2)
- livekit_client (2.4.8):
- Kingfisher (8.3.3)
- livekit_client (2.4.9):
- Flutter
- flutter_webrtc
- WebRTC-SDK (= 125.6422.07)
@ -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:
@ -351,15 +361,16 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327
firebase_core: 700bac7ed92bb754fd70fbf01d72b36ecdd6d450
firebase_messaging: 860c017fcfbb5e27c163062d1d3135388f3ef954
FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0
FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c
FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02
FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: c727a02c560a53f1f1e56e18f16515eb5753c492
firebase_messaging: 4158969b04b667f5435731ec9d6e453bb58b0c4c
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_platform_alert: bf3b5fcd4ac14bd637e20527e9c471633071afd3
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
@ -371,8 +382,8 @@ SPEC CHECKSUMS:
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
irondash_engine_context: 8e58ca8e0212ee9d1c7dc6a42121849986c88486
Kingfisher: 0621d0ac0c78fecb19f6dc5303bde2b52abaf2f5
livekit_client: 9e901890552514206e5ff828903ed271531da264
Kingfisher: ff82cb91d9266ddb56cbb2f72d32c26f00d3e5be
livekit_client: 3f79d79233a5bd13d5b541732624ef959d7c538e
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
media_kit_libs_ios_video: 5a18affdb97d1f5d466dc79988b13eff6c5e2854
media_kit_video: 1746e198cb697d1ffb734b1d05ec429d1fcd1474
@ -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

View File

@ -7,6 +7,7 @@ import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker_android/image_picker_android.dart';
@ -158,6 +159,28 @@ class IslandApp extends HookConsumerWidget {
}
useEffect(() {
const channel = MethodChannel('dev.solsynth.solian/notifications');
Future<void> handleInitialLink() async {
final String? link = await channel.invokeMethod('initialLink');
if (link != null) {
final router = ref.read(routerProvider);
router.go(link);
}
}
if (!kIsWeb && Platform.isAndroid) {
handleInitialLink();
}
channel.setMethodCallHandler((call) async {
if (call.method == 'newLink') {
final String link = call.arguments;
final router = ref.read(routerProvider);
router.go(link);
}
});
// When the app is opened from a terminated state.
FirebaseMessaging.instance.getInitialMessage().then((message) {
if (message != null) {

View File

@ -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<AutoCompletionItem> items,
}) = AutoCompletionAccountResponse;
const factory AutoCompletionResponse.sticker({
required String type,
required List<AutoCompletionItem> items,
}) = AutoCompletionStickerResponse;
factory AutoCompletionResponse.fromJson(Map<String, dynamic> 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<String, dynamic> json) =>
_$AutoCompletionItemFromJson(json);
}

View File

@ -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>(T value) => value;
AutoCompletionResponse _$AutoCompletionResponseFromJson(
Map<String, dynamic> 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<AutoCompletionItem> 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<AutoCompletionResponse> get copyWith => _$AutoCompletionResponseCopyWithImpl<AutoCompletionResponse>(this as AutoCompletionResponse, _$identity);
/// Serializes this AutoCompletionResponse to a JSON map.
Map<String, dynamic> 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<AutoCompletionItem> 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<AutoCompletionItem>,
));
}
}
/// @nodoc
@JsonSerializable()
class AutoCompletionAccountResponse implements AutoCompletionResponse {
const AutoCompletionAccountResponse({required this.type, required final List<AutoCompletionItem> items, final String? $type}): _items = items,$type = $type ?? 'account';
factory AutoCompletionAccountResponse.fromJson(Map<String, dynamic> json) => _$AutoCompletionAccountResponseFromJson(json);
@override final String type;
final List<AutoCompletionItem> _items;
@override List<AutoCompletionItem> 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<AutoCompletionAccountResponse> get copyWith => _$AutoCompletionAccountResponseCopyWithImpl<AutoCompletionAccountResponse>(this, _$identity);
@override
Map<String, dynamic> 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<AutoCompletionItem> 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<AutoCompletionItem>,
));
}
}
/// @nodoc
@JsonSerializable()
class AutoCompletionStickerResponse implements AutoCompletionResponse {
const AutoCompletionStickerResponse({required this.type, required final List<AutoCompletionItem> items, final String? $type}): _items = items,$type = $type ?? 'sticker';
factory AutoCompletionStickerResponse.fromJson(Map<String, dynamic> json) => _$AutoCompletionStickerResponseFromJson(json);
@override final String type;
final List<AutoCompletionItem> _items;
@override List<AutoCompletionItem> 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<AutoCompletionStickerResponse> get copyWith => _$AutoCompletionStickerResponseCopyWithImpl<AutoCompletionStickerResponse>(this, _$identity);
@override
Map<String, dynamic> 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<AutoCompletionItem> 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<AutoCompletionItem>,
));
}
}
/// @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<AutoCompletionItem> get copyWith => _$AutoCompletionItemCopyWithImpl<AutoCompletionItem>(this as AutoCompletionItem, _$identity);
/// Serializes this AutoCompletionItem to a JSON map.
Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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

View File

@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'auto_completion.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
AutoCompletionAccountResponse _$AutoCompletionAccountResponseFromJson(
Map<String, dynamic> json,
) => AutoCompletionAccountResponse(
type: json['type'] as String,
items:
(json['items'] as List<dynamic>)
.map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>))
.toList(),
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$AutoCompletionAccountResponseToJson(
AutoCompletionAccountResponse instance,
) => <String, dynamic>{
'type': instance.type,
'items': instance.items.map((e) => e.toJson()).toList(),
'runtimeType': instance.$type,
};
AutoCompletionStickerResponse _$AutoCompletionStickerResponseFromJson(
Map<String, dynamic> json,
) => AutoCompletionStickerResponse(
type: json['type'] as String,
items:
(json['items'] as List<dynamic>)
.map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>))
.toList(),
$type: json['runtimeType'] as String?,
);
Map<String, dynamic> _$AutoCompletionStickerResponseToJson(
AutoCompletionStickerResponse instance,
) => <String, dynamic>{
'type': instance.type,
'items': instance.items.map((e) => e.toJson()).toList(),
'runtimeType': instance.$type,
};
_AutoCompletionItem _$AutoCompletionItemFromJson(Map<String, dynamic> 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<String, dynamic> _$AutoCompletionItemToJson(_AutoCompletionItem instance) =>
<String, dynamic>{
'id': instance.id,
'display_name': instance.displayName,
'secondary_text': instance.secondaryText,
'type': instance.type,
'data': instance.data,
};

View File

@ -0,0 +1,71 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
part 'custom_app.freezed.dart';
part 'custom_app.g.dart';
@freezed
sealed class CustomApp with _$CustomApp {
const factory CustomApp({
@Default('') String id,
@Default('') String slug,
@Default('') String name,
String? description,
@Default(0) int status,
SnCloudFile? picture,
SnCloudFile? background,
SnVerificationMark? verification,
CustomAppOauthConfig? oauthConfig,
CustomAppLinks? links,
@Default([]) List<CustomAppSecret> secrets,
@Default('') String publisherId,
}) = _CustomApp;
factory CustomApp.fromJson(Map<String, dynamic> json) =>
_$CustomAppFromJson(json);
}
@freezed
sealed class CustomAppLinks with _$CustomAppLinks {
const factory CustomAppLinks({
String? homePage,
String? privacyPolicy,
String? termsOfService,
}) = _CustomAppLinks;
factory CustomAppLinks.fromJson(Map<String, dynamic> json) =>
_$CustomAppLinksFromJson(json);
}
@freezed
sealed class CustomAppOauthConfig with _$CustomAppOauthConfig {
const factory CustomAppOauthConfig({
String? clientUri,
@Default([]) List<String> redirectUris,
List<String>? postLogoutRedirectUris,
@Default(['openid', 'profile', 'email']) List<String> allowedScopes,
@Default(['authorization_code', 'refresh_token'])
List<String> allowedGrantTypes,
@Default(true) bool requirePkce,
@Default(false) bool allowOfflineAccess,
}) = _CustomAppOauthConfig;
factory CustomAppOauthConfig.fromJson(Map<String, dynamic> json) =>
_$CustomAppOauthConfigFromJson(json);
}
@freezed
sealed class CustomAppSecret with _$CustomAppSecret {
const factory CustomAppSecret({
@Default('') String id,
@Default('') String secret,
String? description,
DateTime? expiredAt,
@Default(false) bool isOidc,
@Default('') String appId,
}) = _CustomAppSecret;
factory CustomAppSecret.fromJson(Map<String, dynamic> json) =>
_$CustomAppSecretFromJson(json);
}

View File

@ -0,0 +1,771 @@
// 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 'custom_app.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CustomApp {
String get id; String get slug; String get name; String? get description; int get status; SnCloudFile? get picture; SnCloudFile? get background; SnVerificationMark? get verification; CustomAppOauthConfig? get oauthConfig; CustomAppLinks? get links; List<CustomAppSecret> get secrets; String get publisherId;
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CustomAppCopyWith<CustomApp> get copyWith => _$CustomAppCopyWithImpl<CustomApp>(this as CustomApp, _$identity);
/// Serializes this CustomApp to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomApp&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.status, status) || other.status == status)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.oauthConfig, oauthConfig) || other.oauthConfig == oauthConfig)&&(identical(other.links, links) || other.links == links)&&const DeepCollectionEquality().equals(other.secrets, secrets)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,description,status,picture,background,verification,oauthConfig,links,const DeepCollectionEquality().hash(secrets),publisherId);
@override
String toString() {
return 'CustomApp(id: $id, slug: $slug, name: $name, description: $description, status: $status, picture: $picture, background: $background, verification: $verification, oauthConfig: $oauthConfig, links: $links, secrets: $secrets, publisherId: $publisherId)';
}
}
/// @nodoc
abstract mixin class $CustomAppCopyWith<$Res> {
factory $CustomAppCopyWith(CustomApp value, $Res Function(CustomApp) _then) = _$CustomAppCopyWithImpl;
@useResult
$Res call({
String id, String slug, String name, String? description, int status, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, CustomAppOauthConfig? oauthConfig, CustomAppLinks? links, List<CustomAppSecret> secrets, String publisherId
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnVerificationMarkCopyWith<$Res>? get verification;$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig;$CustomAppLinksCopyWith<$Res>? get links;
}
/// @nodoc
class _$CustomAppCopyWithImpl<$Res>
implements $CustomAppCopyWith<$Res> {
_$CustomAppCopyWithImpl(this._self, this._then);
final CustomApp _self;
final $Res Function(CustomApp) _then;
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? status = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? oauthConfig = freezed,Object? links = freezed,Object? secrets = null,Object? publisherId = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,oauthConfig: freezed == oauthConfig ? _self.oauthConfig : oauthConfig // ignore: cast_nullable_to_non_nullable
as CustomAppOauthConfig?,links: freezed == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
as CustomAppLinks?,secrets: null == secrets ? _self.secrets : secrets // ignore: cast_nullable_to_non_nullable
as List<CustomAppSecret>,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig {
if (_self.oauthConfig == null) {
return null;
}
return $CustomAppOauthConfigCopyWith<$Res>(_self.oauthConfig!, (value) {
return _then(_self.copyWith(oauthConfig: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomAppLinksCopyWith<$Res>? get links {
if (_self.links == null) {
return null;
}
return $CustomAppLinksCopyWith<$Res>(_self.links!, (value) {
return _then(_self.copyWith(links: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _CustomApp implements CustomApp {
const _CustomApp({this.id = '', this.slug = '', this.name = '', this.description, this.status = 0, this.picture, this.background, this.verification, this.oauthConfig, this.links, final List<CustomAppSecret> secrets = const [], this.publisherId = ''}): _secrets = secrets;
factory _CustomApp.fromJson(Map<String, dynamic> json) => _$CustomAppFromJson(json);
@override@JsonKey() final String id;
@override@JsonKey() final String slug;
@override@JsonKey() final String name;
@override final String? description;
@override@JsonKey() final int status;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final SnVerificationMark? verification;
@override final CustomAppOauthConfig? oauthConfig;
@override final CustomAppLinks? links;
final List<CustomAppSecret> _secrets;
@override@JsonKey() List<CustomAppSecret> get secrets {
if (_secrets is EqualUnmodifiableListView) return _secrets;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_secrets);
}
@override@JsonKey() final String publisherId;
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CustomAppCopyWith<_CustomApp> get copyWith => __$CustomAppCopyWithImpl<_CustomApp>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CustomAppToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomApp&&(identical(other.id, id) || other.id == id)&&(identical(other.slug, slug) || other.slug == slug)&&(identical(other.name, name) || other.name == name)&&(identical(other.description, description) || other.description == description)&&(identical(other.status, status) || other.status == status)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.verification, verification) || other.verification == verification)&&(identical(other.oauthConfig, oauthConfig) || other.oauthConfig == oauthConfig)&&(identical(other.links, links) || other.links == links)&&const DeepCollectionEquality().equals(other._secrets, _secrets)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,slug,name,description,status,picture,background,verification,oauthConfig,links,const DeepCollectionEquality().hash(_secrets),publisherId);
@override
String toString() {
return 'CustomApp(id: $id, slug: $slug, name: $name, description: $description, status: $status, picture: $picture, background: $background, verification: $verification, oauthConfig: $oauthConfig, links: $links, secrets: $secrets, publisherId: $publisherId)';
}
}
/// @nodoc
abstract mixin class _$CustomAppCopyWith<$Res> implements $CustomAppCopyWith<$Res> {
factory _$CustomAppCopyWith(_CustomApp value, $Res Function(_CustomApp) _then) = __$CustomAppCopyWithImpl;
@override @useResult
$Res call({
String id, String slug, String name, String? description, int status, SnCloudFile? picture, SnCloudFile? background, SnVerificationMark? verification, CustomAppOauthConfig? oauthConfig, CustomAppLinks? links, List<CustomAppSecret> secrets, String publisherId
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnVerificationMarkCopyWith<$Res>? get verification;@override $CustomAppOauthConfigCopyWith<$Res>? get oauthConfig;@override $CustomAppLinksCopyWith<$Res>? get links;
}
/// @nodoc
class __$CustomAppCopyWithImpl<$Res>
implements _$CustomAppCopyWith<$Res> {
__$CustomAppCopyWithImpl(this._self, this._then);
final _CustomApp _self;
final $Res Function(_CustomApp) _then;
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? slug = null,Object? name = null,Object? description = freezed,Object? status = null,Object? picture = freezed,Object? background = freezed,Object? verification = freezed,Object? oauthConfig = freezed,Object? links = freezed,Object? secrets = null,Object? publisherId = null,}) {
return _then(_CustomApp(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,slug: null == slug ? _self.slug : slug // ignore: cast_nullable_to_non_nullable
as String,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,status: null == status ? _self.status : status // ignore: cast_nullable_to_non_nullable
as int,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,oauthConfig: freezed == oauthConfig ? _self.oauthConfig : oauthConfig // ignore: cast_nullable_to_non_nullable
as CustomAppOauthConfig?,links: freezed == links ? _self.links : links // ignore: cast_nullable_to_non_nullable
as CustomAppLinks?,secrets: null == secrets ? _self._secrets : secrets // ignore: cast_nullable_to_non_nullable
as List<CustomAppSecret>,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomAppOauthConfigCopyWith<$Res>? get oauthConfig {
if (_self.oauthConfig == null) {
return null;
}
return $CustomAppOauthConfigCopyWith<$Res>(_self.oauthConfig!, (value) {
return _then(_self.copyWith(oauthConfig: value));
});
}/// Create a copy of CustomApp
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$CustomAppLinksCopyWith<$Res>? get links {
if (_self.links == null) {
return null;
}
return $CustomAppLinksCopyWith<$Res>(_self.links!, (value) {
return _then(_self.copyWith(links: value));
});
}
}
/// @nodoc
mixin _$CustomAppLinks {
String? get homePage; String? get privacyPolicy; String? get termsOfService;
/// Create a copy of CustomAppLinks
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CustomAppLinksCopyWith<CustomAppLinks> get copyWith => _$CustomAppLinksCopyWithImpl<CustomAppLinks>(this as CustomAppLinks, _$identity);
/// Serializes this CustomAppLinks to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppLinks&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.privacyPolicy, privacyPolicy) || other.privacyPolicy == privacyPolicy)&&(identical(other.termsOfService, termsOfService) || other.termsOfService == termsOfService));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,homePage,privacyPolicy,termsOfService);
@override
String toString() {
return 'CustomAppLinks(homePage: $homePage, privacyPolicy: $privacyPolicy, termsOfService: $termsOfService)';
}
}
/// @nodoc
abstract mixin class $CustomAppLinksCopyWith<$Res> {
factory $CustomAppLinksCopyWith(CustomAppLinks value, $Res Function(CustomAppLinks) _then) = _$CustomAppLinksCopyWithImpl;
@useResult
$Res call({
String? homePage, String? privacyPolicy, String? termsOfService
});
}
/// @nodoc
class _$CustomAppLinksCopyWithImpl<$Res>
implements $CustomAppLinksCopyWith<$Res> {
_$CustomAppLinksCopyWithImpl(this._self, this._then);
final CustomAppLinks _self;
final $Res Function(CustomAppLinks) _then;
/// Create a copy of CustomAppLinks
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? homePage = freezed,Object? privacyPolicy = freezed,Object? termsOfService = freezed,}) {
return _then(_self.copyWith(
homePage: freezed == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable
as String?,privacyPolicy: freezed == privacyPolicy ? _self.privacyPolicy : privacyPolicy // ignore: cast_nullable_to_non_nullable
as String?,termsOfService: freezed == termsOfService ? _self.termsOfService : termsOfService // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
@JsonSerializable()
class _CustomAppLinks implements CustomAppLinks {
const _CustomAppLinks({this.homePage, this.privacyPolicy, this.termsOfService});
factory _CustomAppLinks.fromJson(Map<String, dynamic> json) => _$CustomAppLinksFromJson(json);
@override final String? homePage;
@override final String? privacyPolicy;
@override final String? termsOfService;
/// Create a copy of CustomAppLinks
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CustomAppLinksCopyWith<_CustomAppLinks> get copyWith => __$CustomAppLinksCopyWithImpl<_CustomAppLinks>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CustomAppLinksToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppLinks&&(identical(other.homePage, homePage) || other.homePage == homePage)&&(identical(other.privacyPolicy, privacyPolicy) || other.privacyPolicy == privacyPolicy)&&(identical(other.termsOfService, termsOfService) || other.termsOfService == termsOfService));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,homePage,privacyPolicy,termsOfService);
@override
String toString() {
return 'CustomAppLinks(homePage: $homePage, privacyPolicy: $privacyPolicy, termsOfService: $termsOfService)';
}
}
/// @nodoc
abstract mixin class _$CustomAppLinksCopyWith<$Res> implements $CustomAppLinksCopyWith<$Res> {
factory _$CustomAppLinksCopyWith(_CustomAppLinks value, $Res Function(_CustomAppLinks) _then) = __$CustomAppLinksCopyWithImpl;
@override @useResult
$Res call({
String? homePage, String? privacyPolicy, String? termsOfService
});
}
/// @nodoc
class __$CustomAppLinksCopyWithImpl<$Res>
implements _$CustomAppLinksCopyWith<$Res> {
__$CustomAppLinksCopyWithImpl(this._self, this._then);
final _CustomAppLinks _self;
final $Res Function(_CustomAppLinks) _then;
/// Create a copy of CustomAppLinks
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? homePage = freezed,Object? privacyPolicy = freezed,Object? termsOfService = freezed,}) {
return _then(_CustomAppLinks(
homePage: freezed == homePage ? _self.homePage : homePage // ignore: cast_nullable_to_non_nullable
as String?,privacyPolicy: freezed == privacyPolicy ? _self.privacyPolicy : privacyPolicy // ignore: cast_nullable_to_non_nullable
as String?,termsOfService: freezed == termsOfService ? _self.termsOfService : termsOfService // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
mixin _$CustomAppOauthConfig {
String? get clientUri; List<String> get redirectUris; List<String>? get postLogoutRedirectUris; List<String> get allowedScopes; List<String> get allowedGrantTypes; bool get requirePkce; bool get allowOfflineAccess;
/// Create a copy of CustomAppOauthConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CustomAppOauthConfigCopyWith<CustomAppOauthConfig> get copyWith => _$CustomAppOauthConfigCopyWithImpl<CustomAppOauthConfig>(this as CustomAppOauthConfig, _$identity);
/// Serializes this CustomAppOauthConfig to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppOauthConfig&&(identical(other.clientUri, clientUri) || other.clientUri == clientUri)&&const DeepCollectionEquality().equals(other.redirectUris, redirectUris)&&const DeepCollectionEquality().equals(other.postLogoutRedirectUris, postLogoutRedirectUris)&&const DeepCollectionEquality().equals(other.allowedScopes, allowedScopes)&&const DeepCollectionEquality().equals(other.allowedGrantTypes, allowedGrantTypes)&&(identical(other.requirePkce, requirePkce) || other.requirePkce == requirePkce)&&(identical(other.allowOfflineAccess, allowOfflineAccess) || other.allowOfflineAccess == allowOfflineAccess));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,clientUri,const DeepCollectionEquality().hash(redirectUris),const DeepCollectionEquality().hash(postLogoutRedirectUris),const DeepCollectionEquality().hash(allowedScopes),const DeepCollectionEquality().hash(allowedGrantTypes),requirePkce,allowOfflineAccess);
@override
String toString() {
return 'CustomAppOauthConfig(clientUri: $clientUri, redirectUris: $redirectUris, postLogoutRedirectUris: $postLogoutRedirectUris, allowedScopes: $allowedScopes, allowedGrantTypes: $allowedGrantTypes, requirePkce: $requirePkce, allowOfflineAccess: $allowOfflineAccess)';
}
}
/// @nodoc
abstract mixin class $CustomAppOauthConfigCopyWith<$Res> {
factory $CustomAppOauthConfigCopyWith(CustomAppOauthConfig value, $Res Function(CustomAppOauthConfig) _then) = _$CustomAppOauthConfigCopyWithImpl;
@useResult
$Res call({
String? clientUri, List<String> redirectUris, List<String>? postLogoutRedirectUris, List<String> allowedScopes, List<String> allowedGrantTypes, bool requirePkce, bool allowOfflineAccess
});
}
/// @nodoc
class _$CustomAppOauthConfigCopyWithImpl<$Res>
implements $CustomAppOauthConfigCopyWith<$Res> {
_$CustomAppOauthConfigCopyWithImpl(this._self, this._then);
final CustomAppOauthConfig _self;
final $Res Function(CustomAppOauthConfig) _then;
/// Create a copy of CustomAppOauthConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? clientUri = freezed,Object? redirectUris = null,Object? postLogoutRedirectUris = freezed,Object? allowedScopes = null,Object? allowedGrantTypes = null,Object? requirePkce = null,Object? allowOfflineAccess = null,}) {
return _then(_self.copyWith(
clientUri: freezed == clientUri ? _self.clientUri : clientUri // ignore: cast_nullable_to_non_nullable
as String?,redirectUris: null == redirectUris ? _self.redirectUris : redirectUris // ignore: cast_nullable_to_non_nullable
as List<String>,postLogoutRedirectUris: freezed == postLogoutRedirectUris ? _self.postLogoutRedirectUris : postLogoutRedirectUris // ignore: cast_nullable_to_non_nullable
as List<String>?,allowedScopes: null == allowedScopes ? _self.allowedScopes : allowedScopes // ignore: cast_nullable_to_non_nullable
as List<String>,allowedGrantTypes: null == allowedGrantTypes ? _self.allowedGrantTypes : allowedGrantTypes // ignore: cast_nullable_to_non_nullable
as List<String>,requirePkce: null == requirePkce ? _self.requirePkce : requirePkce // ignore: cast_nullable_to_non_nullable
as bool,allowOfflineAccess: null == allowOfflineAccess ? _self.allowOfflineAccess : allowOfflineAccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _CustomAppOauthConfig implements CustomAppOauthConfig {
const _CustomAppOauthConfig({this.clientUri, final List<String> redirectUris = const [], final List<String>? postLogoutRedirectUris, final List<String> allowedScopes = const ['openid', 'profile', 'email'], final List<String> allowedGrantTypes = const ['authorization_code', 'refresh_token'], this.requirePkce = true, this.allowOfflineAccess = false}): _redirectUris = redirectUris,_postLogoutRedirectUris = postLogoutRedirectUris,_allowedScopes = allowedScopes,_allowedGrantTypes = allowedGrantTypes;
factory _CustomAppOauthConfig.fromJson(Map<String, dynamic> json) => _$CustomAppOauthConfigFromJson(json);
@override final String? clientUri;
final List<String> _redirectUris;
@override@JsonKey() List<String> get redirectUris {
if (_redirectUris is EqualUnmodifiableListView) return _redirectUris;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_redirectUris);
}
final List<String>? _postLogoutRedirectUris;
@override List<String>? get postLogoutRedirectUris {
final value = _postLogoutRedirectUris;
if (value == null) return null;
if (_postLogoutRedirectUris is EqualUnmodifiableListView) return _postLogoutRedirectUris;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<String> _allowedScopes;
@override@JsonKey() List<String> get allowedScopes {
if (_allowedScopes is EqualUnmodifiableListView) return _allowedScopes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_allowedScopes);
}
final List<String> _allowedGrantTypes;
@override@JsonKey() List<String> get allowedGrantTypes {
if (_allowedGrantTypes is EqualUnmodifiableListView) return _allowedGrantTypes;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_allowedGrantTypes);
}
@override@JsonKey() final bool requirePkce;
@override@JsonKey() final bool allowOfflineAccess;
/// Create a copy of CustomAppOauthConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CustomAppOauthConfigCopyWith<_CustomAppOauthConfig> get copyWith => __$CustomAppOauthConfigCopyWithImpl<_CustomAppOauthConfig>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CustomAppOauthConfigToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppOauthConfig&&(identical(other.clientUri, clientUri) || other.clientUri == clientUri)&&const DeepCollectionEquality().equals(other._redirectUris, _redirectUris)&&const DeepCollectionEquality().equals(other._postLogoutRedirectUris, _postLogoutRedirectUris)&&const DeepCollectionEquality().equals(other._allowedScopes, _allowedScopes)&&const DeepCollectionEquality().equals(other._allowedGrantTypes, _allowedGrantTypes)&&(identical(other.requirePkce, requirePkce) || other.requirePkce == requirePkce)&&(identical(other.allowOfflineAccess, allowOfflineAccess) || other.allowOfflineAccess == allowOfflineAccess));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,clientUri,const DeepCollectionEquality().hash(_redirectUris),const DeepCollectionEquality().hash(_postLogoutRedirectUris),const DeepCollectionEquality().hash(_allowedScopes),const DeepCollectionEquality().hash(_allowedGrantTypes),requirePkce,allowOfflineAccess);
@override
String toString() {
return 'CustomAppOauthConfig(clientUri: $clientUri, redirectUris: $redirectUris, postLogoutRedirectUris: $postLogoutRedirectUris, allowedScopes: $allowedScopes, allowedGrantTypes: $allowedGrantTypes, requirePkce: $requirePkce, allowOfflineAccess: $allowOfflineAccess)';
}
}
/// @nodoc
abstract mixin class _$CustomAppOauthConfigCopyWith<$Res> implements $CustomAppOauthConfigCopyWith<$Res> {
factory _$CustomAppOauthConfigCopyWith(_CustomAppOauthConfig value, $Res Function(_CustomAppOauthConfig) _then) = __$CustomAppOauthConfigCopyWithImpl;
@override @useResult
$Res call({
String? clientUri, List<String> redirectUris, List<String>? postLogoutRedirectUris, List<String> allowedScopes, List<String> allowedGrantTypes, bool requirePkce, bool allowOfflineAccess
});
}
/// @nodoc
class __$CustomAppOauthConfigCopyWithImpl<$Res>
implements _$CustomAppOauthConfigCopyWith<$Res> {
__$CustomAppOauthConfigCopyWithImpl(this._self, this._then);
final _CustomAppOauthConfig _self;
final $Res Function(_CustomAppOauthConfig) _then;
/// Create a copy of CustomAppOauthConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? clientUri = freezed,Object? redirectUris = null,Object? postLogoutRedirectUris = freezed,Object? allowedScopes = null,Object? allowedGrantTypes = null,Object? requirePkce = null,Object? allowOfflineAccess = null,}) {
return _then(_CustomAppOauthConfig(
clientUri: freezed == clientUri ? _self.clientUri : clientUri // ignore: cast_nullable_to_non_nullable
as String?,redirectUris: null == redirectUris ? _self._redirectUris : redirectUris // ignore: cast_nullable_to_non_nullable
as List<String>,postLogoutRedirectUris: freezed == postLogoutRedirectUris ? _self._postLogoutRedirectUris : postLogoutRedirectUris // ignore: cast_nullable_to_non_nullable
as List<String>?,allowedScopes: null == allowedScopes ? _self._allowedScopes : allowedScopes // ignore: cast_nullable_to_non_nullable
as List<String>,allowedGrantTypes: null == allowedGrantTypes ? _self._allowedGrantTypes : allowedGrantTypes // ignore: cast_nullable_to_non_nullable
as List<String>,requirePkce: null == requirePkce ? _self.requirePkce : requirePkce // ignore: cast_nullable_to_non_nullable
as bool,allowOfflineAccess: null == allowOfflineAccess ? _self.allowOfflineAccess : allowOfflineAccess // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
mixin _$CustomAppSecret {
String get id; String get secret; String? get description; DateTime? get expiredAt; bool get isOidc; String get appId;
/// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$CustomAppSecretCopyWith<CustomAppSecret> get copyWith => _$CustomAppSecretCopyWithImpl<CustomAppSecret>(this as CustomAppSecret, _$identity);
/// Serializes this CustomAppSecret to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)&&(identical(other.appId, appId) || other.appId == appId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,secret,description,expiredAt,isOidc,appId);
@override
String toString() {
return 'CustomAppSecret(id: $id, secret: $secret, description: $description, expiredAt: $expiredAt, isOidc: $isOidc, appId: $appId)';
}
}
/// @nodoc
abstract mixin class $CustomAppSecretCopyWith<$Res> {
factory $CustomAppSecretCopyWith(CustomAppSecret value, $Res Function(CustomAppSecret) _then) = _$CustomAppSecretCopyWithImpl;
@useResult
$Res call({
String id, String secret, String? description, DateTime? expiredAt, bool isOidc, String appId
});
}
/// @nodoc
class _$CustomAppSecretCopyWithImpl<$Res>
implements $CustomAppSecretCopyWith<$Res> {
_$CustomAppSecretCopyWithImpl(this._self, this._then);
final CustomAppSecret _self;
final $Res Function(CustomAppSecret) _then;
/// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? secret = null,Object? description = freezed,Object? expiredAt = freezed,Object? isOidc = null,Object? appId = null,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isOidc: null == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
as bool,appId: null == appId ? _self.appId : appId // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
/// @nodoc
@JsonSerializable()
class _CustomAppSecret implements CustomAppSecret {
const _CustomAppSecret({this.id = '', this.secret = '', this.description, this.expiredAt, this.isOidc = false, this.appId = ''});
factory _CustomAppSecret.fromJson(Map<String, dynamic> json) => _$CustomAppSecretFromJson(json);
@override@JsonKey() final String id;
@override@JsonKey() final String secret;
@override final String? description;
@override final DateTime? expiredAt;
@override@JsonKey() final bool isOidc;
@override@JsonKey() final String appId;
/// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$CustomAppSecretCopyWith<_CustomAppSecret> get copyWith => __$CustomAppSecretCopyWithImpl<_CustomAppSecret>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$CustomAppSecretToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomAppSecret&&(identical(other.id, id) || other.id == id)&&(identical(other.secret, secret) || other.secret == secret)&&(identical(other.description, description) || other.description == description)&&(identical(other.expiredAt, expiredAt) || other.expiredAt == expiredAt)&&(identical(other.isOidc, isOidc) || other.isOidc == isOidc)&&(identical(other.appId, appId) || other.appId == appId));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,secret,description,expiredAt,isOidc,appId);
@override
String toString() {
return 'CustomAppSecret(id: $id, secret: $secret, description: $description, expiredAt: $expiredAt, isOidc: $isOidc, appId: $appId)';
}
}
/// @nodoc
abstract mixin class _$CustomAppSecretCopyWith<$Res> implements $CustomAppSecretCopyWith<$Res> {
factory _$CustomAppSecretCopyWith(_CustomAppSecret value, $Res Function(_CustomAppSecret) _then) = __$CustomAppSecretCopyWithImpl;
@override @useResult
$Res call({
String id, String secret, String? description, DateTime? expiredAt, bool isOidc, String appId
});
}
/// @nodoc
class __$CustomAppSecretCopyWithImpl<$Res>
implements _$CustomAppSecretCopyWith<$Res> {
__$CustomAppSecretCopyWithImpl(this._self, this._then);
final _CustomAppSecret _self;
final $Res Function(_CustomAppSecret) _then;
/// Create a copy of CustomAppSecret
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? secret = null,Object? description = freezed,Object? expiredAt = freezed,Object? isOidc = null,Object? appId = null,}) {
return _then(_CustomAppSecret(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,secret: null == secret ? _self.secret : secret // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,expiredAt: freezed == expiredAt ? _self.expiredAt : expiredAt // ignore: cast_nullable_to_non_nullable
as DateTime?,isOidc: null == isOidc ? _self.isOidc : isOidc // ignore: cast_nullable_to_non_nullable
as bool,appId: null == appId ? _self.appId : appId // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@ -0,0 +1,137 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'custom_app.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_CustomApp _$CustomAppFromJson(Map<String, dynamic> json) => _CustomApp(
id: json['id'] as String? ?? '',
slug: json['slug'] as String? ?? '',
name: json['name'] as String? ?? '',
description: json['description'] as String?,
status: (json['status'] as num?)?.toInt() ?? 0,
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
oauthConfig:
json['oauth_config'] == null
? null
: CustomAppOauthConfig.fromJson(
json['oauth_config'] as Map<String, dynamic>,
),
links:
json['links'] == null
? null
: CustomAppLinks.fromJson(json['links'] as Map<String, dynamic>),
secrets:
(json['secrets'] as List<dynamic>?)
?.map((e) => CustomAppSecret.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
publisherId: json['publisher_id'] as String? ?? '',
);
Map<String, dynamic> _$CustomAppToJson(_CustomApp instance) =>
<String, dynamic>{
'id': instance.id,
'slug': instance.slug,
'name': instance.name,
'description': instance.description,
'status': instance.status,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'verification': instance.verification?.toJson(),
'oauth_config': instance.oauthConfig?.toJson(),
'links': instance.links?.toJson(),
'secrets': instance.secrets.map((e) => e.toJson()).toList(),
'publisher_id': instance.publisherId,
};
_CustomAppLinks _$CustomAppLinksFromJson(Map<String, dynamic> json) =>
_CustomAppLinks(
homePage: json['home_page'] as String?,
privacyPolicy: json['privacy_policy'] as String?,
termsOfService: json['terms_of_service'] as String?,
);
Map<String, dynamic> _$CustomAppLinksToJson(_CustomAppLinks instance) =>
<String, dynamic>{
'home_page': instance.homePage,
'privacy_policy': instance.privacyPolicy,
'terms_of_service': instance.termsOfService,
};
_CustomAppOauthConfig _$CustomAppOauthConfigFromJson(
Map<String, dynamic> json,
) => _CustomAppOauthConfig(
clientUri: json['client_uri'] as String?,
redirectUris:
(json['redirect_uris'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
postLogoutRedirectUris:
(json['post_logout_redirect_uris'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
allowedScopes:
(json['allowed_scopes'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ['openid', 'profile', 'email'],
allowedGrantTypes:
(json['allowed_grant_types'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const ['authorization_code', 'refresh_token'],
requirePkce: json['require_pkce'] as bool? ?? true,
allowOfflineAccess: json['allow_offline_access'] as bool? ?? false,
);
Map<String, dynamic> _$CustomAppOauthConfigToJson(
_CustomAppOauthConfig instance,
) => <String, dynamic>{
'client_uri': instance.clientUri,
'redirect_uris': instance.redirectUris,
'post_logout_redirect_uris': instance.postLogoutRedirectUris,
'allowed_scopes': instance.allowedScopes,
'allowed_grant_types': instance.allowedGrantTypes,
'require_pkce': instance.requirePkce,
'allow_offline_access': instance.allowOfflineAccess,
};
_CustomAppSecret _$CustomAppSecretFromJson(Map<String, dynamic> json) =>
_CustomAppSecret(
id: json['id'] as String? ?? '',
secret: json['secret'] as String? ?? '',
description: json['description'] as String?,
expiredAt:
json['expired_at'] == null
? null
: DateTime.parse(json['expired_at'] as String),
isOidc: json['is_oidc'] as bool? ?? false,
appId: json['app_id'] as String? ?? '',
);
Map<String, dynamic> _$CustomAppSecretToJson(_CustomAppSecret instance) =>
<String, dynamic>{
'id': instance.id,
'secret': instance.secret,
'description': instance.description,
'expired_at': instance.expiredAt?.toIso8601String(),
'is_oidc': instance.isOidc,
'app_id': instance.appId,
};

14
lib/models/developer.dart Normal file
View File

@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'developer.freezed.dart';
part 'developer.g.dart';
@freezed
sealed class DeveloperStats with _$DeveloperStats {
const factory DeveloperStats({
@Default(0) int totalCustomApps,
}) = _DeveloperStats;
factory DeveloperStats.fromJson(Map<String, dynamic> json) =>
_$DeveloperStatsFromJson(json);
}

View File

@ -0,0 +1,148 @@
// 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 'developer.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$DeveloperStats {
int get totalCustomApps;
/// Create a copy of DeveloperStats
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$DeveloperStatsCopyWith<DeveloperStats> get copyWith => _$DeveloperStatsCopyWithImpl<DeveloperStats>(this as DeveloperStats, _$identity);
/// Serializes this DeveloperStats to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is DeveloperStats&&(identical(other.totalCustomApps, totalCustomApps) || other.totalCustomApps == totalCustomApps));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,totalCustomApps);
@override
String toString() {
return 'DeveloperStats(totalCustomApps: $totalCustomApps)';
}
}
/// @nodoc
abstract mixin class $DeveloperStatsCopyWith<$Res> {
factory $DeveloperStatsCopyWith(DeveloperStats value, $Res Function(DeveloperStats) _then) = _$DeveloperStatsCopyWithImpl;
@useResult
$Res call({
int totalCustomApps
});
}
/// @nodoc
class _$DeveloperStatsCopyWithImpl<$Res>
implements $DeveloperStatsCopyWith<$Res> {
_$DeveloperStatsCopyWithImpl(this._self, this._then);
final DeveloperStats _self;
final $Res Function(DeveloperStats) _then;
/// Create a copy of DeveloperStats
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? totalCustomApps = null,}) {
return _then(_self.copyWith(
totalCustomApps: null == totalCustomApps ? _self.totalCustomApps : totalCustomApps // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
@JsonSerializable()
class _DeveloperStats implements DeveloperStats {
const _DeveloperStats({this.totalCustomApps = 0});
factory _DeveloperStats.fromJson(Map<String, dynamic> json) => _$DeveloperStatsFromJson(json);
@override@JsonKey() final int totalCustomApps;
/// Create a copy of DeveloperStats
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$DeveloperStatsCopyWith<_DeveloperStats> get copyWith => __$DeveloperStatsCopyWithImpl<_DeveloperStats>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$DeveloperStatsToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DeveloperStats&&(identical(other.totalCustomApps, totalCustomApps) || other.totalCustomApps == totalCustomApps));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,totalCustomApps);
@override
String toString() {
return 'DeveloperStats(totalCustomApps: $totalCustomApps)';
}
}
/// @nodoc
abstract mixin class _$DeveloperStatsCopyWith<$Res> implements $DeveloperStatsCopyWith<$Res> {
factory _$DeveloperStatsCopyWith(_DeveloperStats value, $Res Function(_DeveloperStats) _then) = __$DeveloperStatsCopyWithImpl;
@override @useResult
$Res call({
int totalCustomApps
});
}
/// @nodoc
class __$DeveloperStatsCopyWithImpl<$Res>
implements _$DeveloperStatsCopyWith<$Res> {
__$DeveloperStatsCopyWithImpl(this._self, this._then);
final _DeveloperStats _self;
final $Res Function(_DeveloperStats) _then;
/// Create a copy of DeveloperStats
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? totalCustomApps = null,}) {
return _then(_DeveloperStats(
totalCustomApps: null == totalCustomApps ? _self.totalCustomApps : totalCustomApps // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
// dart format on

View File

@ -0,0 +1,15 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'developer.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_DeveloperStats _$DeveloperStatsFromJson(Map<String, dynamic> json) =>
_DeveloperStats(
totalCustomApps: (json['total_custom_apps'] as num?)?.toInt() ?? 0,
);
Map<String, dynamic> _$DeveloperStatsToJson(_DeveloperStats instance) =>
<String, dynamic>{'total_custom_apps': instance.totalCustomApps};

View File

@ -2,7 +2,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post_category.dart';
import 'package:island/models/post_tag.dart';
import 'package:island/models/user.dart';
import 'package:island/models/publisher.dart';
part 'post.freezed.dart';
part 'post.g.dart';
@ -32,7 +32,7 @@ sealed class SnPost with _$SnPost {
String? forwardedPostId,
SnPost? forwardedPost,
@Default([]) List<SnCloudFile> attachments,
@Default(SnPublisher()) SnPublisher publisher,
required SnPublisher publisher,
@Default({}) Map<String, int> reactionsCount,
@Default([]) List<dynamic> reactions,
@Default([]) List<PostTag> tags,
@ -47,29 +47,6 @@ sealed class SnPost with _$SnPost {
factory SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
}
@freezed
sealed class SnPublisher with _$SnPublisher {
const factory SnPublisher({
@Default('') String id,
@Default(0) int type,
@Default('') String name,
@Default('') String nick,
@Default('') String bio,
SnCloudFile? picture,
SnCloudFile? background,
SnAccount? account,
String? accountId,
@Default(null) DateTime? createdAt,
@Default(null) DateTime? updatedAt,
DateTime? deletedAt,
String? realmId,
SnVerificationMark? verification,
}) = _SnPublisher;
factory SnPublisher.fromJson(Map<String, dynamic> json) =>
_$SnPublisherFromJson(json);
}
@freezed
sealed class SnPublisherStats with _$SnPublisherStats {
const factory SnPublisherStats({

View File

@ -156,7 +156,7 @@ $SnPublisherCopyWith<$Res> get publisher {
@JsonSerializable()
class _SnPost implements SnPost {
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final List<SnCloudFile> attachments = const [], this.publisher = const SnPublisher(), final Map<String, int> reactionsCount = const {}, final List<dynamic> reactions = const [], final List<PostTag> tags = const [], final List<PostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
const _SnPost({required this.id, this.title, this.description, this.language, this.editedAt, this.publishedAt = null, this.visibility = 0, this.content, this.type = 0, final Map<String, dynamic>? meta, this.viewsUnique = 0, this.viewsTotal = 0, this.upvotes = 0, this.downvotes = 0, this.repliesCount = 0, this.threadedPostId, this.threadedPost, this.repliedPostId, this.repliedPost, this.forwardedPostId, this.forwardedPost, final List<SnCloudFile> attachments = const [], required this.publisher, final Map<String, int> reactionsCount = const {}, final List<dynamic> reactions = const [], final List<PostTag> tags = const [], final List<PostCategory> categories = const [], final List<dynamic> collections = const [], this.createdAt = null, this.updatedAt = null, this.deletedAt, this.isTruncated = false}): _meta = meta,_attachments = attachments,_reactionsCount = reactionsCount,_reactions = reactions,_tags = tags,_categories = categories,_collections = collections;
factory _SnPost.fromJson(Map<String, dynamic> json) => _$SnPostFromJson(json);
@override final String id;
@ -195,7 +195,7 @@ class _SnPost implements SnPost {
return EqualUnmodifiableListView(_attachments);
}
@override@JsonKey() final SnPublisher publisher;
@override final SnPublisher publisher;
final Map<String, int> _reactionsCount;
@override@JsonKey() Map<String, int> get reactionsCount {
if (_reactionsCount is EqualUnmodifiableMapView) return _reactionsCount;
@ -373,274 +373,6 @@ $SnPublisherCopyWith<$Res> get publisher {
}
/// @nodoc
mixin _$SnPublisher {
String get id; int get type; String get name; String get nick; String get bio; SnCloudFile? get picture; SnCloudFile? get background; SnAccount? get account; String? get accountId; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; String? get realmId; SnVerificationMark? get verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<SnPublisher> get copyWith => _$SnPublisherCopyWithImpl<SnPublisher>(this as SnPublisher, _$identity);
/// Serializes this SnPublisher to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
}
/// @nodoc
abstract mixin class $SnPublisherCopyWith<$Res> {
factory $SnPublisherCopyWith(SnPublisher value, $Res Function(SnPublisher) _then) = _$SnPublisherCopyWithImpl;
@useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnAccountCopyWith<$Res>? get account;$SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
class _$SnPublisherCopyWithImpl<$Res>
implements $SnPublisherCopyWith<$Res> {
_$SnPublisherCopyWithImpl(this._self, this._then);
final SnPublisher _self;
final $Res Function(SnPublisher) _then;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
as String,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnPublisher implements SnPublisher {
const _SnPublisher({this.id = '', this.type = 0, this.name = '', this.nick = '', this.bio = '', this.picture, this.background, this.account, this.accountId, this.createdAt = null, this.updatedAt = null, this.deletedAt, this.realmId, this.verification});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => _$SnPublisherFromJson(json);
@override@JsonKey() final String id;
@override@JsonKey() final int type;
@override@JsonKey() final String name;
@override@JsonKey() final String nick;
@override@JsonKey() final String bio;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final SnAccount? account;
@override final String? accountId;
@override@JsonKey() final DateTime? createdAt;
@override@JsonKey() final DateTime? updatedAt;
@override final DateTime? deletedAt;
@override final String? realmId;
@override final SnVerificationMark? verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPublisherCopyWith<_SnPublisher> get copyWith => __$SnPublisherCopyWithImpl<_SnPublisher>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPublisherToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
}
/// @nodoc
abstract mixin class _$SnPublisherCopyWith<$Res> implements $SnPublisherCopyWith<$Res> {
factory _$SnPublisherCopyWith(_SnPublisher value, $Res Function(_SnPublisher) _then) = __$SnPublisherCopyWithImpl;
@override @useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnAccountCopyWith<$Res>? get account;@override $SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
class __$SnPublisherCopyWithImpl<$Res>
implements _$SnPublisherCopyWith<$Res> {
__$SnPublisherCopyWithImpl(this._self, this._then);
final _SnPublisher _self;
final $Res Function(_SnPublisher) _then;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_SnPublisher(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
as String,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
/// @nodoc
mixin _$SnPublisherStats {

View File

@ -48,10 +48,7 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
publisher:
json['publisher'] == null
? const SnPublisher()
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
publisher: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
reactionsCount:
(json['reactions_count'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, (e as num).toInt()),
@ -119,64 +116,6 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
'is_truncated': instance.isTruncated,
};
_SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
id: json['id'] as String? ?? '',
type: (json['type'] as num?)?.toInt() ?? 0,
name: json['name'] as String? ?? '',
nick: json['nick'] as String? ?? '',
bio: json['bio'] as String? ?? '',
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: json['account_id'] as String?,
createdAt:
json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
updatedAt:
json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
realmId: json['realm_id'] as String?,
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'name': instance.name,
'nick': instance.nick,
'bio': instance.bio,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'account': instance.account?.toJson(),
'account_id': instance.accountId,
'created_at': instance.createdAt?.toIso8601String(),
'updated_at': instance.updatedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'realm_id': instance.realmId,
'verification': instance.verification?.toJson(),
};
_SnPublisherStats _$SnPublisherStatsFromJson(Map<String, dynamic> json) =>
_SnPublisherStats(
postsCreated: (json['posts_created'] as num).toInt(),

47
lib/models/publisher.dart Normal file
View File

@ -0,0 +1,47 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/user.dart';
part 'publisher.freezed.dart';
part 'publisher.g.dart';
@freezed
sealed class SnPublisher with _$SnPublisher {
const factory SnPublisher({
@Default('') String id,
@Default(0) int type,
@Default('') String name,
@Default('') String nick,
@Default('') String bio,
SnCloudFile? picture,
SnCloudFile? background,
SnAccount? account,
String? accountId,
@Default(null) DateTime? createdAt,
@Default(null) DateTime? updatedAt,
DateTime? deletedAt,
String? realmId,
SnVerificationMark? verification,
}) = _SnPublisher;
factory SnPublisher.fromJson(Map<String, dynamic> json) =>
_$SnPublisherFromJson(json);
}
@freezed
sealed class SnPublisherMember with _$SnPublisherMember {
const factory SnPublisherMember({
required String publisherId,
required SnPublisher? publisher,
required String accountId,
required SnAccount? account,
required int role,
required DateTime? joinedAt,
required DateTime createdAt,
required DateTime updatedAt,
required DateTime? deletedAt,
}) = _SnPublisherMember;
factory SnPublisherMember.fromJson(Map<String, dynamic> json) =>
_$SnPublisherMemberFromJson(json);
}

View File

@ -0,0 +1,488 @@
// 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 'publisher.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnPublisher {
String get id; int get type; String get name; String get nick; String get bio; SnCloudFile? get picture; SnCloudFile? get background; SnAccount? get account; String? get accountId; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; String? get realmId; SnVerificationMark? get verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<SnPublisher> get copyWith => _$SnPublisherCopyWithImpl<SnPublisher>(this as SnPublisher, _$identity);
/// Serializes this SnPublisher to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
}
/// @nodoc
abstract mixin class $SnPublisherCopyWith<$Res> {
factory $SnPublisherCopyWith(SnPublisher value, $Res Function(SnPublisher) _then) = _$SnPublisherCopyWithImpl;
@useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
$SnCloudFileCopyWith<$Res>? get picture;$SnCloudFileCopyWith<$Res>? get background;$SnAccountCopyWith<$Res>? get account;$SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
class _$SnPublisherCopyWithImpl<$Res>
implements $SnPublisherCopyWith<$Res> {
_$SnPublisherCopyWithImpl(this._self, this._then);
final SnPublisher _self;
final $Res Function(SnPublisher) _then;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
as String,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnPublisher implements SnPublisher {
const _SnPublisher({this.id = '', this.type = 0, this.name = '', this.nick = '', this.bio = '', this.picture, this.background, this.account, this.accountId, this.createdAt = null, this.updatedAt = null, this.deletedAt, this.realmId, this.verification});
factory _SnPublisher.fromJson(Map<String, dynamic> json) => _$SnPublisherFromJson(json);
@override@JsonKey() final String id;
@override@JsonKey() final int type;
@override@JsonKey() final String name;
@override@JsonKey() final String nick;
@override@JsonKey() final String bio;
@override final SnCloudFile? picture;
@override final SnCloudFile? background;
@override final SnAccount? account;
@override final String? accountId;
@override@JsonKey() final DateTime? createdAt;
@override@JsonKey() final DateTime? updatedAt;
@override final DateTime? deletedAt;
@override final String? realmId;
@override final SnVerificationMark? verification;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPublisherCopyWith<_SnPublisher> get copyWith => __$SnPublisherCopyWithImpl<_SnPublisher>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPublisherToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisher&&(identical(other.id, id) || other.id == id)&&(identical(other.type, type) || other.type == type)&&(identical(other.name, name) || other.name == name)&&(identical(other.nick, nick) || other.nick == nick)&&(identical(other.bio, bio) || other.bio == bio)&&(identical(other.picture, picture) || other.picture == picture)&&(identical(other.background, background) || other.background == background)&&(identical(other.account, account) || other.account == account)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt)&&(identical(other.realmId, realmId) || other.realmId == realmId)&&(identical(other.verification, verification) || other.verification == verification));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,type,name,nick,bio,picture,background,account,accountId,createdAt,updatedAt,deletedAt,realmId,verification);
@override
String toString() {
return 'SnPublisher(id: $id, type: $type, name: $name, nick: $nick, bio: $bio, picture: $picture, background: $background, account: $account, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, realmId: $realmId, verification: $verification)';
}
}
/// @nodoc
abstract mixin class _$SnPublisherCopyWith<$Res> implements $SnPublisherCopyWith<$Res> {
factory _$SnPublisherCopyWith(_SnPublisher value, $Res Function(_SnPublisher) _then) = __$SnPublisherCopyWithImpl;
@override @useResult
$Res call({
String id, int type, String name, String nick, String bio, SnCloudFile? picture, SnCloudFile? background, SnAccount? account, String? accountId, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, String? realmId, SnVerificationMark? verification
});
@override $SnCloudFileCopyWith<$Res>? get picture;@override $SnCloudFileCopyWith<$Res>? get background;@override $SnAccountCopyWith<$Res>? get account;@override $SnVerificationMarkCopyWith<$Res>? get verification;
}
/// @nodoc
class __$SnPublisherCopyWithImpl<$Res>
implements _$SnPublisherCopyWith<$Res> {
__$SnPublisherCopyWithImpl(this._self, this._then);
final _SnPublisher _self;
final $Res Function(_SnPublisher) _then;
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? type = null,Object? name = null,Object? nick = null,Object? bio = null,Object? picture = freezed,Object? background = freezed,Object? account = freezed,Object? accountId = freezed,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
return _then(_SnPublisher(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable
as String,nick: null == nick ? _self.nick : nick // ignore: cast_nullable_to_non_nullable
as String,bio: null == bio ? _self.bio : bio // ignore: cast_nullable_to_non_nullable
as String,picture: freezed == picture ? _self.picture : picture // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,background: freezed == background ? _self.background : background // ignore: cast_nullable_to_non_nullable
as SnCloudFile?,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,accountId: freezed == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime?,updatedAt: freezed == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,realmId: freezed == realmId ? _self.realmId : realmId // ignore: cast_nullable_to_non_nullable
as String?,verification: freezed == verification ? _self.verification : verification // ignore: cast_nullable_to_non_nullable
as SnVerificationMark?,
));
}
/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get picture {
if (_self.picture == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.picture!, (value) {
return _then(_self.copyWith(picture: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnCloudFileCopyWith<$Res>? get background {
if (_self.background == null) {
return null;
}
return $SnCloudFileCopyWith<$Res>(_self.background!, (value) {
return _then(_self.copyWith(background: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}/// Create a copy of SnPublisher
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnVerificationMarkCopyWith<$Res>? get verification {
if (_self.verification == null) {
return null;
}
return $SnVerificationMarkCopyWith<$Res>(_self.verification!, (value) {
return _then(_self.copyWith(verification: value));
});
}
}
/// @nodoc
mixin _$SnPublisherMember {
String get publisherId; SnPublisher? get publisher; String get accountId; SnAccount? get account; int get role; DateTime? get joinedAt; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnPublisherMemberCopyWith<SnPublisherMember> get copyWith => _$SnPublisherMemberCopyWithImpl<SnPublisherMember>(this as SnPublisherMember, _$identity);
/// Serializes this SnPublisherMember to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPublisherMember&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.role, role) || other.role == role)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,publisherId,publisher,accountId,account,role,joinedAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnPublisherMember(publisherId: $publisherId, publisher: $publisher, accountId: $accountId, account: $account, role: $role, joinedAt: $joinedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnPublisherMemberCopyWith<$Res> {
factory $SnPublisherMemberCopyWith(SnPublisherMember value, $Res Function(SnPublisherMember) _then) = _$SnPublisherMemberCopyWithImpl;
@useResult
$Res call({
String publisherId, SnPublisher? publisher, String accountId, SnAccount? account, int role, DateTime? joinedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnPublisherCopyWith<$Res>? get publisher;$SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
class _$SnPublisherMemberCopyWithImpl<$Res>
implements $SnPublisherMemberCopyWith<$Res> {
_$SnPublisherMemberCopyWithImpl(this._self, this._then);
final SnPublisherMember _self;
final $Res Function(SnPublisherMember) _then;
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? publisherId = null,Object? publisher = freezed,Object? accountId = null,Object? account = freezed,Object? role = null,Object? joinedAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res>? get publisher {
if (_self.publisher == null) {
return null;
}
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
return _then(_self.copyWith(publisher: value));
});
}/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnPublisherMember implements SnPublisherMember {
const _SnPublisherMember({required this.publisherId, required this.publisher, required this.accountId, required this.account, required this.role, required this.joinedAt, required this.createdAt, required this.updatedAt, required this.deletedAt});
factory _SnPublisherMember.fromJson(Map<String, dynamic> json) => _$SnPublisherMemberFromJson(json);
@override final String publisherId;
@override final SnPublisher? publisher;
@override final String accountId;
@override final SnAccount? account;
@override final int role;
@override final DateTime? joinedAt;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnPublisherMemberCopyWith<_SnPublisherMember> get copyWith => __$SnPublisherMemberCopyWithImpl<_SnPublisherMember>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnPublisherMemberToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPublisherMember&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.publisher, publisher) || other.publisher == publisher)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(identical(other.account, account) || other.account == account)&&(identical(other.role, role) || other.role == role)&&(identical(other.joinedAt, joinedAt) || other.joinedAt == joinedAt)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,publisherId,publisher,accountId,account,role,joinedAt,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnPublisherMember(publisherId: $publisherId, publisher: $publisher, accountId: $accountId, account: $account, role: $role, joinedAt: $joinedAt, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnPublisherMemberCopyWith<$Res> implements $SnPublisherMemberCopyWith<$Res> {
factory _$SnPublisherMemberCopyWith(_SnPublisherMember value, $Res Function(_SnPublisherMember) _then) = __$SnPublisherMemberCopyWithImpl;
@override @useResult
$Res call({
String publisherId, SnPublisher? publisher, String accountId, SnAccount? account, int role, DateTime? joinedAt, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnPublisherCopyWith<$Res>? get publisher;@override $SnAccountCopyWith<$Res>? get account;
}
/// @nodoc
class __$SnPublisherMemberCopyWithImpl<$Res>
implements _$SnPublisherMemberCopyWith<$Res> {
__$SnPublisherMemberCopyWithImpl(this._self, this._then);
final _SnPublisherMember _self;
final $Res Function(_SnPublisherMember) _then;
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? publisherId = null,Object? publisher = freezed,Object? accountId = null,Object? account = freezed,Object? role = null,Object? joinedAt = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnPublisherMember(
publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,publisher: freezed == publisher ? _self.publisher : publisher // ignore: cast_nullable_to_non_nullable
as SnPublisher?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,account: freezed == account ? _self.account : account // ignore: cast_nullable_to_non_nullable
as SnAccount?,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as int,joinedAt: freezed == joinedAt ? _self.joinedAt : joinedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnPublisherCopyWith<$Res>? get publisher {
if (_self.publisher == null) {
return null;
}
return $SnPublisherCopyWith<$Res>(_self.publisher!, (value) {
return _then(_self.copyWith(publisher: value));
});
}/// Create a copy of SnPublisherMember
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnAccountCopyWith<$Res>? get account {
if (_self.account == null) {
return null;
}
return $SnAccountCopyWith<$Res>(_self.account!, (value) {
return _then(_self.copyWith(account: value));
});
}
}
// dart format on

103
lib/models/publisher.g.dart Normal file
View File

@ -0,0 +1,103 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'publisher.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
id: json['id'] as String? ?? '',
type: (json['type'] as num?)?.toInt() ?? 0,
name: json['name'] as String? ?? '',
nick: json['nick'] as String? ?? '',
bio: json['bio'] as String? ?? '',
picture:
json['picture'] == null
? null
: SnCloudFile.fromJson(json['picture'] as Map<String, dynamic>),
background:
json['background'] == null
? null
: SnCloudFile.fromJson(json['background'] as Map<String, dynamic>),
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
accountId: json['account_id'] as String?,
createdAt:
json['created_at'] == null
? null
: DateTime.parse(json['created_at'] as String),
updatedAt:
json['updated_at'] == null
? null
: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
realmId: json['realm_id'] as String?,
verification:
json['verification'] == null
? null
: SnVerificationMark.fromJson(
json['verification'] as Map<String, dynamic>,
),
);
Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
<String, dynamic>{
'id': instance.id,
'type': instance.type,
'name': instance.name,
'nick': instance.nick,
'bio': instance.bio,
'picture': instance.picture?.toJson(),
'background': instance.background?.toJson(),
'account': instance.account?.toJson(),
'account_id': instance.accountId,
'created_at': instance.createdAt?.toIso8601String(),
'updated_at': instance.updatedAt?.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
'realm_id': instance.realmId,
'verification': instance.verification?.toJson(),
};
_SnPublisherMember _$SnPublisherMemberFromJson(Map<String, dynamic> json) =>
_SnPublisherMember(
publisherId: json['publisher_id'] as String,
publisher:
json['publisher'] == null
? null
: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
accountId: json['account_id'] as String,
account:
json['account'] == null
? null
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
role: (json['role'] as num).toInt(),
joinedAt:
json['joined_at'] == null
? null
: DateTime.parse(json['joined_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnPublisherMemberToJson(_SnPublisherMember instance) =>
<String, dynamic>{
'publisher_id': instance.publisherId,
'publisher': instance.publisher?.toJson(),
'account_id': instance.accountId,
'account': instance.account?.toJson(),
'role': instance.role,
'joined_at': instance.joinedAt?.toIso8601String(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -1,6 +1,6 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
part 'sticker.freezed.dart';
part 'sticker.g.dart';

64
lib/models/webfeed.dart Normal file
View File

@ -0,0 +1,64 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/models/embed.dart';
part 'webfeed.freezed.dart';
part 'webfeed.g.dart';
@freezed
sealed class SnWebFeedConfig with _$SnWebFeedConfig {
const factory SnWebFeedConfig({@Default(false) bool scrapPage}) =
_SnWebFeedConfig;
factory SnWebFeedConfig.fromJson(Map<String, dynamic> json) =>
_$SnWebFeedConfigFromJson(json);
}
@freezed
sealed class SnWebFeed with _$SnWebFeed {
const factory SnWebFeed({
required String id,
required String url,
required String title,
String? description,
SnScrappedLink? preview,
@Default(SnWebFeedConfig()) SnWebFeedConfig config,
required String publisherId,
@Default([]) List<SnWebArticle> articles,
required DateTime createdAt,
required DateTime updatedAt,
DateTime? deletedAt,
}) = _SnWebFeed;
factory SnWebFeed.fromJson(Map<String, dynamic> json) =>
_$SnWebFeedFromJson(json);
factory SnWebFeed.fromJsonString(String jsonString) =>
SnWebFeed.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
}
@freezed
sealed class SnWebArticle with _$SnWebArticle {
const factory SnWebArticle({
required String id,
required String title,
required String url,
String? author,
Map<String, dynamic>? meta,
SnScrappedLink? preview,
SnWebFeed? feed,
String? content,
DateTime? publishedAt,
required String feedId,
required DateTime createdAt,
required DateTime updatedAt,
DateTime? deletedAt,
}) = _SnWebArticle;
factory SnWebArticle.fromJson(Map<String, dynamic> json) =>
_$SnWebArticleFromJson(json);
factory SnWebArticle.fromJsonString(String jsonString) =>
SnWebArticle.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
}

View File

@ -0,0 +1,584 @@
// 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 'webfeed.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SnWebFeedConfig {
bool get scrapPage;
/// Create a copy of SnWebFeedConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnWebFeedConfigCopyWith<SnWebFeedConfig> get copyWith => _$SnWebFeedConfigCopyWithImpl<SnWebFeedConfig>(this as SnWebFeedConfig, _$identity);
/// Serializes this SnWebFeedConfig to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,scrapPage);
@override
String toString() {
return 'SnWebFeedConfig(scrapPage: $scrapPage)';
}
}
/// @nodoc
abstract mixin class $SnWebFeedConfigCopyWith<$Res> {
factory $SnWebFeedConfigCopyWith(SnWebFeedConfig value, $Res Function(SnWebFeedConfig) _then) = _$SnWebFeedConfigCopyWithImpl;
@useResult
$Res call({
bool scrapPage
});
}
/// @nodoc
class _$SnWebFeedConfigCopyWithImpl<$Res>
implements $SnWebFeedConfigCopyWith<$Res> {
_$SnWebFeedConfigCopyWithImpl(this._self, this._then);
final SnWebFeedConfig _self;
final $Res Function(SnWebFeedConfig) _then;
/// Create a copy of SnWebFeedConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? scrapPage = null,}) {
return _then(_self.copyWith(
scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@JsonSerializable()
class _SnWebFeedConfig implements SnWebFeedConfig {
const _SnWebFeedConfig({this.scrapPage = false});
factory _SnWebFeedConfig.fromJson(Map<String, dynamic> json) => _$SnWebFeedConfigFromJson(json);
@override@JsonKey() final bool scrapPage;
/// Create a copy of SnWebFeedConfig
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnWebFeedConfigCopyWith<_SnWebFeedConfig> get copyWith => __$SnWebFeedConfigCopyWithImpl<_SnWebFeedConfig>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnWebFeedConfigToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,scrapPage);
@override
String toString() {
return 'SnWebFeedConfig(scrapPage: $scrapPage)';
}
}
/// @nodoc
abstract mixin class _$SnWebFeedConfigCopyWith<$Res> implements $SnWebFeedConfigCopyWith<$Res> {
factory _$SnWebFeedConfigCopyWith(_SnWebFeedConfig value, $Res Function(_SnWebFeedConfig) _then) = __$SnWebFeedConfigCopyWithImpl;
@override @useResult
$Res call({
bool scrapPage
});
}
/// @nodoc
class __$SnWebFeedConfigCopyWithImpl<$Res>
implements _$SnWebFeedConfigCopyWith<$Res> {
__$SnWebFeedConfigCopyWithImpl(this._self, this._then);
final _SnWebFeedConfig _self;
final $Res Function(_SnWebFeedConfig) _then;
/// Create a copy of SnWebFeedConfig
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? scrapPage = null,}) {
return _then(_SnWebFeedConfig(
scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
mixin _$SnWebFeed {
String get id; String get url; String get title; String? get description; SnScrappedLink? get preview; SnWebFeedConfig get config; String get publisherId; List<SnWebArticle> get articles; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnWebFeedCopyWith<SnWebFeed> get copyWith => _$SnWebFeedCopyWithImpl<SnWebFeed>(this as SnWebFeed, _$identity);
/// Serializes this SnWebFeed to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebFeed&&(identical(other.id, id) || other.id == id)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.config, config) || other.config == config)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&const DeepCollectionEquality().equals(other.articles, articles)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,url,title,description,preview,config,publisherId,const DeepCollectionEquality().hash(articles),createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWebFeed(id: $id, url: $url, title: $title, description: $description, preview: $preview, config: $config, publisherId: $publisherId, articles: $articles, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnWebFeedCopyWith<$Res> {
factory $SnWebFeedCopyWith(SnWebFeed value, $Res Function(SnWebFeed) _then) = _$SnWebFeedCopyWithImpl;
@useResult
$Res call({
String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnScrappedLinkCopyWith<$Res>? get preview;$SnWebFeedConfigCopyWith<$Res> get config;
}
/// @nodoc
class _$SnWebFeedCopyWithImpl<$Res>
implements $SnWebFeedCopyWith<$Res> {
_$SnWebFeedCopyWithImpl(this._self, this._then);
final SnWebFeed _self;
final $Res Function(SnWebFeed) _then;
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? url = null,Object? title = null,Object? description = freezed,Object? preview = freezed,Object? config = null,Object? publisherId = null,Object? articles = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
as SnScrappedLink?,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
as SnWebFeedConfig,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,articles: null == articles ? _self.articles : articles // ignore: cast_nullable_to_non_nullable
as List<SnWebArticle>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnScrappedLinkCopyWith<$Res>? get preview {
if (_self.preview == null) {
return null;
}
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
return _then(_self.copyWith(preview: value));
});
}/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWebFeedConfigCopyWith<$Res> get config {
return $SnWebFeedConfigCopyWith<$Res>(_self.config, (value) {
return _then(_self.copyWith(config: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnWebFeed implements SnWebFeed {
const _SnWebFeed({required this.id, required this.url, required this.title, this.description, this.preview, this.config = const SnWebFeedConfig(), required this.publisherId, final List<SnWebArticle> articles = const [], required this.createdAt, required this.updatedAt, this.deletedAt}): _articles = articles;
factory _SnWebFeed.fromJson(Map<String, dynamic> json) => _$SnWebFeedFromJson(json);
@override final String id;
@override final String url;
@override final String title;
@override final String? description;
@override final SnScrappedLink? preview;
@override@JsonKey() final SnWebFeedConfig config;
@override final String publisherId;
final List<SnWebArticle> _articles;
@override@JsonKey() List<SnWebArticle> get articles {
if (_articles is EqualUnmodifiableListView) return _articles;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_articles);
}
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnWebFeedCopyWith<_SnWebFeed> get copyWith => __$SnWebFeedCopyWithImpl<_SnWebFeed>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnWebFeedToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebFeed&&(identical(other.id, id) || other.id == id)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.config, config) || other.config == config)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&const DeepCollectionEquality().equals(other._articles, _articles)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,url,title,description,preview,config,publisherId,const DeepCollectionEquality().hash(_articles),createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWebFeed(id: $id, url: $url, title: $title, description: $description, preview: $preview, config: $config, publisherId: $publisherId, articles: $articles, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnWebFeedCopyWith<$Res> implements $SnWebFeedCopyWith<$Res> {
factory _$SnWebFeedCopyWith(_SnWebFeed value, $Res Function(_SnWebFeed) _then) = __$SnWebFeedCopyWithImpl;
@override @useResult
$Res call({
String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnScrappedLinkCopyWith<$Res>? get preview;@override $SnWebFeedConfigCopyWith<$Res> get config;
}
/// @nodoc
class __$SnWebFeedCopyWithImpl<$Res>
implements _$SnWebFeedCopyWith<$Res> {
__$SnWebFeedCopyWithImpl(this._self, this._then);
final _SnWebFeed _self;
final $Res Function(_SnWebFeed) _then;
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? url = null,Object? title = null,Object? description = freezed,Object? preview = freezed,Object? config = null,Object? publisherId = null,Object? articles = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnWebFeed(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
as String?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
as SnScrappedLink?,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
as SnWebFeedConfig,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
as String,articles: null == articles ? _self._articles : articles // ignore: cast_nullable_to_non_nullable
as List<SnWebArticle>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnScrappedLinkCopyWith<$Res>? get preview {
if (_self.preview == null) {
return null;
}
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
return _then(_self.copyWith(preview: value));
});
}/// Create a copy of SnWebFeed
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWebFeedConfigCopyWith<$Res> get config {
return $SnWebFeedConfigCopyWith<$Res>(_self.config, (value) {
return _then(_self.copyWith(config: value));
});
}
}
/// @nodoc
mixin _$SnWebArticle {
String get id; String get title; String get url; String? get author; Map<String, dynamic>? get meta; SnScrappedLink? get preview; SnWebFeed? get feed; String? get content; DateTime? get publishedAt; String get feedId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnWebArticleCopyWith<SnWebArticle> get copyWith => _$SnWebArticleCopyWithImpl<SnWebArticle>(this as SnWebArticle, _$identity);
/// Serializes this SnWebArticle to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebArticle&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.url, url) || other.url == url)&&(identical(other.author, author) || other.author == author)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.feed, feed) || other.feed == feed)&&(identical(other.content, content) || other.content == content)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,title,url,author,const DeepCollectionEquality().hash(meta),preview,feed,content,publishedAt,feedId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, feed: $feed, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class $SnWebArticleCopyWith<$Res> {
factory $SnWebArticleCopyWith(SnWebArticle value, $Res Function(SnWebArticle) _then) = _$SnWebArticleCopyWithImpl;
@useResult
$Res call({
String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
$SnScrappedLinkCopyWith<$Res>? get preview;$SnWebFeedCopyWith<$Res>? get feed;
}
/// @nodoc
class _$SnWebArticleCopyWithImpl<$Res>
implements $SnWebArticleCopyWith<$Res> {
_$SnWebArticleCopyWithImpl(this._self, this._then);
final SnWebArticle _self;
final $Res Function(SnWebArticle) _then;
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = null,Object? url = null,Object? author = freezed,Object? meta = freezed,Object? preview = freezed,Object? feed = freezed,Object? content = freezed,Object? publishedAt = freezed,Object? feedId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
as SnScrappedLink?,feed: freezed == feed ? _self.feed : feed // ignore: cast_nullable_to_non_nullable
as SnWebFeed?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,feedId: null == feedId ? _self.feedId : feedId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnScrappedLinkCopyWith<$Res>? get preview {
if (_self.preview == null) {
return null;
}
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
return _then(_self.copyWith(preview: value));
});
}/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWebFeedCopyWith<$Res>? get feed {
if (_self.feed == null) {
return null;
}
return $SnWebFeedCopyWith<$Res>(_self.feed!, (value) {
return _then(_self.copyWith(feed: value));
});
}
}
/// @nodoc
@JsonSerializable()
class _SnWebArticle implements SnWebArticle {
const _SnWebArticle({required this.id, required this.title, required this.url, this.author, final Map<String, dynamic>? meta, this.preview, this.feed, this.content, this.publishedAt, required this.feedId, required this.createdAt, required this.updatedAt, this.deletedAt}): _meta = meta;
factory _SnWebArticle.fromJson(Map<String, dynamic> json) => _$SnWebArticleFromJson(json);
@override final String id;
@override final String title;
@override final String url;
@override final String? author;
final Map<String, dynamic>? _meta;
@override Map<String, dynamic>? get meta {
final value = _meta;
if (value == null) return null;
if (_meta is EqualUnmodifiableMapView) return _meta;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override final SnScrappedLink? preview;
@override final SnWebFeed? feed;
@override final String? content;
@override final DateTime? publishedAt;
@override final String feedId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@override final DateTime? deletedAt;
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnWebArticleCopyWith<_SnWebArticle> get copyWith => __$SnWebArticleCopyWithImpl<_SnWebArticle>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnWebArticleToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebArticle&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.url, url) || other.url == url)&&(identical(other.author, author) || other.author == author)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.feed, feed) || other.feed == feed)&&(identical(other.content, content) || other.content == content)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,title,url,author,const DeepCollectionEquality().hash(_meta),preview,feed,content,publishedAt,feedId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnWebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, feed: $feed, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
}
/// @nodoc
abstract mixin class _$SnWebArticleCopyWith<$Res> implements $SnWebArticleCopyWith<$Res> {
factory _$SnWebArticleCopyWith(_SnWebArticle value, $Res Function(_SnWebArticle) _then) = __$SnWebArticleCopyWithImpl;
@override @useResult
$Res call({
String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@override $SnScrappedLinkCopyWith<$Res>? get preview;@override $SnWebFeedCopyWith<$Res>? get feed;
}
/// @nodoc
class __$SnWebArticleCopyWithImpl<$Res>
implements _$SnWebArticleCopyWith<$Res> {
__$SnWebArticleCopyWithImpl(this._self, this._then);
final _SnWebArticle _self;
final $Res Function(_SnWebArticle) _then;
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = null,Object? url = null,Object? author = freezed,Object? meta = freezed,Object? preview = freezed,Object? feed = freezed,Object? content = freezed,Object? publishedAt = freezed,Object? feedId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnWebArticle(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
as String?,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
as SnScrappedLink?,feed: freezed == feed ? _self.feed : feed // ignore: cast_nullable_to_non_nullable
as SnWebFeed?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,feedId: null == feedId ? _self.feedId : feedId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
as DateTime?,
));
}
/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnScrappedLinkCopyWith<$Res>? get preview {
if (_self.preview == null) {
return null;
}
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
return _then(_self.copyWith(preview: value));
});
}/// Create a copy of SnWebArticle
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SnWebFeedCopyWith<$Res>? get feed {
if (_self.feed == null) {
return null;
}
return $SnWebFeedCopyWith<$Res>(_self.feed!, (value) {
return _then(_self.copyWith(feed: value));
});
}
}
// dart format on

103
lib/models/webfeed.g.dart Normal file
View File

@ -0,0 +1,103 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'webfeed.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_SnWebFeedConfig _$SnWebFeedConfigFromJson(Map<String, dynamic> json) =>
_SnWebFeedConfig(scrapPage: json['scrap_page'] as bool? ?? false);
Map<String, dynamic> _$SnWebFeedConfigToJson(_SnWebFeedConfig instance) =>
<String, dynamic>{'scrap_page': instance.scrapPage};
_SnWebFeed _$SnWebFeedFromJson(Map<String, dynamic> json) => _SnWebFeed(
id: json['id'] as String,
url: json['url'] as String,
title: json['title'] as String,
description: json['description'] as String?,
preview:
json['preview'] == null
? null
: SnScrappedLink.fromJson(json['preview'] as Map<String, dynamic>),
config:
json['config'] == null
? const SnWebFeedConfig()
: SnWebFeedConfig.fromJson(json['config'] as Map<String, dynamic>),
publisherId: json['publisher_id'] as String,
articles:
(json['articles'] as List<dynamic>?)
?.map((e) => SnWebArticle.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnWebFeedToJson(_SnWebFeed instance) =>
<String, dynamic>{
'id': instance.id,
'url': instance.url,
'title': instance.title,
'description': instance.description,
'preview': instance.preview?.toJson(),
'config': instance.config.toJson(),
'publisher_id': instance.publisherId,
'articles': instance.articles.map((e) => e.toJson()).toList(),
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};
_SnWebArticle _$SnWebArticleFromJson(Map<String, dynamic> json) =>
_SnWebArticle(
id: json['id'] as String,
title: json['title'] as String,
url: json['url'] as String,
author: json['author'] as String?,
meta: json['meta'] as Map<String, dynamic>?,
preview:
json['preview'] == null
? null
: SnScrappedLink.fromJson(
json['preview'] as Map<String, dynamic>,
),
feed:
json['feed'] == null
? null
: SnWebFeed.fromJson(json['feed'] as Map<String, dynamic>),
content: json['content'] as String?,
publishedAt:
json['published_at'] == null
? null
: DateTime.parse(json['published_at'] as String),
feedId: json['feed_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
deletedAt:
json['deleted_at'] == null
? null
: DateTime.parse(json['deleted_at'] as String),
);
Map<String, dynamic> _$SnWebArticleToJson(_SnWebArticle instance) =>
<String, dynamic>{
'id': instance.id,
'title': instance.title,
'url': instance.url,
'author': instance.author,
'meta': instance.meta,
'preview': instance.preview?.toJson(),
'feed': instance.feed?.toJson(),
'content': instance.content,
'published_at': instance.publishedAt?.toIso8601String(),
'feed_id': instance.feedId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
'deleted_at': instance.deletedAt?.toIso8601String(),
};

View File

@ -0,0 +1,31 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:dio/dio.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart';
/// Provider that fetches a single article by its ID
final articleDetailProvider = FutureProvider.autoDispose.family<SnWebArticle, String>(
(ref, articleId) async {
final dio = ref.watch(apiClientProvider);
try {
final response = await dio.get<Map<String, dynamic>>(
'/feeds/articles/$articleId',
);
if (response.statusCode == 200 && response.data != null) {
return SnWebArticle.fromJson(response.data!);
} else {
throw Exception('Failed to load article');
}
} on DioException catch (e) {
if (e.response?.statusCode == 404) {
throw Exception('Article not found');
} else {
throw Exception('Failed to load article: ${e.message}');
}
} catch (e) {
throw Exception('Failed to load article: $e');
}
},
);

View File

@ -0,0 +1 @@

View File

@ -11,11 +11,6 @@ class UserInfoNotifier extends StateNotifier<AsyncValue<SnAccount?>> {
UserInfoNotifier(this._ref) : super(const AsyncValue.data(null));
Future<String?> getAccessToken() async {
final prefs = _ref.read(sharedPreferencesProvider);
return prefs.getString(kTokenPairStoreKey);
}
Future<void> fetchUser() async {
try {
final client = _ref.read(apiClientProvider);

123
lib/pods/webfeed.dart Normal file
View File

@ -0,0 +1,123 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart';
final webFeedListProvider = FutureProvider.family<List<SnWebFeed>, String>((
ref,
pubName,
) async {
final client = ref.watch(apiClientProvider);
final response = await client.get('/publishers/$pubName/feeds');
return (response.data as List)
.map((json) => SnWebFeed.fromJson(json))
.toList();
});
class WebFeedNotifier
extends
AutoDisposeFamilyAsyncNotifier<
SnWebFeed,
({String pubName, String? feedId})
> {
@override
FutureOr<SnWebFeed> build(({String pubName, String? feedId}) arg) async {
if (arg.feedId == null || arg.feedId!.isEmpty) {
return SnWebFeed(
id: '',
url: '',
title: '',
publisherId: arg.pubName,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
);
}
try {
final client = ref.read(apiClientProvider);
final response = await client.get(
'/publishers/${arg.pubName}/feeds/${arg.feedId}',
);
return SnWebFeed.fromJson(response.data);
} catch (e) {
rethrow;
}
}
Future<void> saveFeed(SnWebFeed feed) async {
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
final url = '/publishers/${feed.publisherId}/feeds';
final response =
feed.id.isEmpty
? await client.post(url, data: feed.toJson())
: await client.patch('$url/${feed.id}', data: feed.toJson());
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
}
}
Future<void> deleteFeed() async {
final feedId = arg.feedId;
if (feedId == null || feedId.isEmpty) return;
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
await client.delete('/publishers/${arg.pubName}/feeds/$feedId');
state = AsyncValue.data(
SnWebFeed(
id: '',
url: '',
title: '',
publisherId: arg.pubName,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
),
);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
}
}
Future<void> scrapFeed() async {
final feedId = arg.feedId;
if (feedId == null || feedId.isEmpty) return;
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
await client.post(
'/publishers/${arg.pubName}/feeds/$feedId/scrap',
options: Options(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 180),
),
);
// Reload the feed
final response = await client.get(
'/publishers/${arg.pubName}/feeds/$feedId',
);
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
rethrow;
}
}
}
final webFeedNotifierProvider = AsyncNotifierProvider.autoDispose
.family<WebFeedNotifier, SnWebFeed, ({String pubName, String? feedId})>(
WebFeedNotifier.new,
);

View File

@ -1,10 +1,17 @@
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';
import 'package:island/screens/developers/hub.dart';
import 'package:island/screens/discovery/articles.dart';
import 'package:island/screens/posts/post_search.dart';
import 'package:island/widgets/app_wrapper.dart';
import 'package:island/screens/tabs.dart';
import 'package:island/screens/explore.dart';
import 'package:island/screens/article_detail_screen.dart';
import 'package:island/screens/account.dart';
import 'package:island/screens/notification.dart';
import 'package:island/screens/wallet.dart';
@ -18,18 +25,20 @@ import 'package:island/screens/chat/room.dart';
import 'package:island/screens/chat/room_detail.dart';
import 'package:island/screens/chat/call.dart';
import 'package:island/screens/creators/hub.dart';
import 'package:island/screens/creators/posts/list.dart';
import 'package:island/screens/creators/posts/post_manage_list.dart';
import 'package:island/screens/creators/stickers/stickers.dart';
import 'package:island/screens/creators/stickers/pack_detail.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/screens/creators/webfeed/webfeed_list.dart';
import 'package:island/screens/creators/webfeed/webfeed_edit.dart';
import 'package:island/screens/posts/compose.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/screens/posts/pub_profile.dart';
import 'package:island/screens/auth/login.dart';
import 'package:island/screens/auth/create_account.dart';
import 'package:island/screens/settings.dart';
import 'package:island/screens/realm/realms.dart';
import 'package:island/screens/realm/detail.dart';
import 'package:island/screens/realm/realm_detail.dart';
import 'package:island/screens/account/event_calendar.dart';
import 'package:island/screens/discovery/realms.dart';
@ -79,33 +88,64 @@ final routerProvider = Provider<GoRouter>((ref) {
return EventCalanderScreen(name: name);
},
),
GoRoute(
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
ShellRoute(
builder:
(context, state, child) => CreatorHubShellScreen(child: child),
routes: [
GoRoute(
path: ':name/posts',
path: '/creators',
builder: (context, state) => const CreatorHubScreen(),
),
// Web Feed Routes
GoRoute(
path: '/creators/:name/feeds',
builder: (context, state) {
final name = state.pathParameters['name']!;
return WebFeedListScreen(pubName: name);
},
routes: [
GoRoute(
path: 'new',
builder: (context, state) {
return WebFeedNewScreen(
pubName: state.pathParameters['name']!,
);
},
),
GoRoute(
path: ':feedId',
builder: (context, state) {
return WebFeedEditScreen(
pubName: state.pathParameters['name']!,
feedId: state.pathParameters['feedId'],
);
},
),
],
),
GoRoute(
path: '/creators/:name/posts',
builder: (context, state) {
final name = state.pathParameters['name']!;
return CreatorPostListScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers',
path: '/creators/:name/stickers',
builder: (context, state) {
final name = state.pathParameters['name']!;
return StickersScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers/new',
path: '/creators/:name/stickers/new',
builder: (context, state) {
final name = state.pathParameters['name']!;
return NewStickerPacksScreen(pubName: name);
},
),
GoRoute(
path: ':name/stickers/:packId/edit',
path: '/creators/:name/stickers/:packId/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
@ -113,7 +153,7 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: ':name/stickers/:packId',
path: '/creators/:name/stickers/:packId',
builder: (context, state) {
final name = state.pathParameters['name']!;
final packId = state.pathParameters['packId']!;
@ -121,14 +161,14 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: ':name/stickers/:packId/new',
path: '/creators/:name/stickers/:packId/new',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
return NewStickersScreen(packId: packId);
},
),
GoRoute(
path: ':name/stickers/:packId/:id/edit',
path: '/creators/:name/stickers/:packId/:id/edit',
builder: (context, state) {
final packId = state.pathParameters['packId']!;
final id = state.pathParameters['id']!;
@ -136,11 +176,11 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
GoRoute(
path: 'new',
path: '/creators/new',
builder: (context, state) => const NewPublisherScreen(),
),
GoRoute(
path: ':name/edit',
path: '/creators/:name/edit',
builder: (context, state) {
final name = state.pathParameters['name']!;
return EditPublisherScreen(name: name);
@ -148,6 +188,52 @@ final routerProvider = Provider<GoRouter>((ref) {
),
],
),
ShellRoute(
builder:
(context, state, child) =>
DeveloperHubShellScreen(child: child),
routes: [
GoRoute(
path: '/developers',
builder: (context, state) => const DeveloperHubScreen(),
),
GoRoute(
path: '/developers/:name/apps',
builder:
(context, state) => CustomAppsScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
path: '/developers/:name/apps/new',
builder:
(context, state) => NewCustomAppScreen(
publisherName: state.pathParameters['name']!,
),
),
GoRoute(
path: '/developers/:name/apps/:id',
builder:
(context, state) => EditAppScreen(
publisherName: state.pathParameters['name']!,
id: state.pathParameters['id']!,
),
),
],
),
// Web articles
GoRoute(
path: '/feeds/articles',
builder: (context, state) => const ArticlesScreen(),
),
GoRoute(
path: '/feeds/articles/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ArticleDetailScreen(articleId: id);
},
),
// Auth routes
GoRoute(
@ -164,6 +250,10 @@ final routerProvider = Provider<GoRouter>((ref) {
path: '/settings',
builder: (context, state) => const SettingsScreen(),
),
GoRoute(
path: '/about',
builder: (context, state) => const AboutScreen(),
),
// Main tabs with TabsScreen shell
ShellRoute(
@ -173,56 +263,68 @@ final routerProvider = Provider<GoRouter>((ref) {
},
routes: [
// Explore tab
GoRoute(
path: '/',
builder: (context, state) => const ExploreScreen(),
ShellRoute(
builder:
(context, state, child) => ExploreShellScreen(child: child),
routes: [
GoRoute(
path: 'posts/:id',
path: '/',
builder: (context, state) => const ExploreScreen(),
),
GoRoute(
path: '/posts/search',
builder: (context, state) => const PostSearchScreen(),
),
GoRoute(
path: '/posts/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return PostDetailScreen(id: id);
},
),
GoRoute(
path: 'publishers/:name',
path: '/publishers/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return PublisherProfileScreen(name: name);
},
),
GoRoute(
path: 'discovery/realms',
path: '/discovery/realms',
builder: (context, state) => const DiscoveryRealmsScreen(),
),
],
),
// Chat tab
GoRoute(
path: '/chat',
builder: (context, state) => const ChatListScreen(),
ShellRoute(
builder:
(context, state, child) => ChatShellScreen(child: child),
routes: [
GoRoute(
path: 'new',
path: '/chat',
builder: (context, state) => const ChatListScreen(),
),
GoRoute(
path: '/chat/new',
builder: (context, state) => const NewChatScreen(),
),
GoRoute(
path: ':id',
path: '/chat/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatRoomScreen(id: id);
},
),
GoRoute(
path: ':id/edit',
path: '/chat/:id/edit',
builder: (context, state) {
final id = state.pathParameters['id']!;
return EditChatScreen(id: id);
},
),
GoRoute(
path: ':id/detail',
path: '/chat/:id/detail',
builder: (context, state) {
final id = state.pathParameters['id']!;
return ChatDetailScreen(id: id);
@ -258,39 +360,43 @@ final routerProvider = Provider<GoRouter>((ref) {
),
// Account tab
GoRoute(
path: '/account',
builder: (context, state) => const AccountScreen(),
ShellRoute(
builder:
(context, state, child) => AccountShellScreen(child: child),
routes: [
GoRoute(
path: 'notifications',
path: '/account',
builder: (context, state) => const AccountScreen(),
),
GoRoute(
path: '/account/notifications',
builder: (context, state) => const NotificationScreen(),
),
GoRoute(
path: 'wallet',
path: '/account/wallet',
builder: (context, state) => const WalletScreen(),
),
GoRoute(
path: 'relationships',
path: '/account/relationships',
builder: (context, state) => const RelationshipScreen(),
),
GoRoute(
path: ':name',
path: '/account/:name',
builder: (context, state) {
final name = state.pathParameters['name']!;
return AccountProfileScreen(name: name);
},
),
GoRoute(
path: 'me/update',
path: '/account/me/update',
builder: (context, state) => const UpdateProfileScreen(),
),
GoRoute(
path: 'me/leveling',
path: '/account/me/leveling',
builder: (context, state) => const LevelingScreen(),
),
GoRoute(
path: 'settings',
path: '/account/settings',
builder: (context, state) => const AccountSettingsScreen(),
),
],

300
lib/screens/about.dart Normal file
View File

@ -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<AboutScreen> createState() => _AboutScreenState();
}
class _AboutScreenState extends State<AboutScreen> {
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<void> _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<void> _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<Widget> 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,
),
],
);
}
}

View File

@ -143,7 +143,7 @@ class AccountScreen extends HookConsumerWidget {
progress: user.value!.profile.levelingProgress,
),
onTap: () {
context.push('/account/leveling');
context.push('/account/me/leveling');
},
).padding(horizontal: 12),
Row(
@ -178,7 +178,9 @@ class AccountScreen extends HookConsumerWidget {
Text('developerPortalDescription').tr(),
],
).padding(horizontal: 16, vertical: 12),
onTap: () {},
onTap: () {
context.push('/developers');
},
),
).height(140),
),
@ -210,7 +212,7 @@ class AccountScreen extends HookConsumerWidget {
contentPadding: EdgeInsets.symmetric(horizontal: 24),
title: Text('wallet').tr(),
onTap: () {
context.push('/wallet');
context.push('/account/wallet');
},
),
ListTile(
@ -279,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),

View File

@ -341,7 +341,10 @@ class UpdateProfileScreen extends HookConsumerWidget {
),
TextFormField(
decoration: InputDecoration(labelText: 'bio'.tr()),
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
),
maxLines: null,
minLines: 3,
controller: bioController,

View File

@ -53,17 +53,21 @@ Future<List<SnAccountBadge>> accountBadges(Ref ref, String uname) async {
@riverpod
Future<Color?> accountAppbarForcegroundColor(Ref ref, String uname) async {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
try {
final account = await ref.watch(accountProvider(uname).future);
if (account.profile.background == null) return null;
final palette = await PaletteGenerator.fromImageProvider(
CloudImageWidget.provider(
fileId: account.profile.background!.id,
serverUrl: ref.watch(serverUrlProvider),
),
);
final dominantColor = palette.dominantColor?.color;
if (dominantColor == null) return null;
return dominantColor.computeLuminance() > 0.5 ? Colors.black : Colors.white;
} catch (_) {
return null;
}
}
@riverpod

View File

@ -268,7 +268,7 @@ class _AccountBadgesProviderElement
}
String _$accountAppbarForcegroundColorHash() =>
r'f654a7a5594eda1500906e9ad023c22772257a9b';
r'8ee0cae10817b77fb09548a482f5247662b4374c';
/// See also [accountAppbarForcegroundColor].
@ProviderFor(accountAppbarForcegroundColor)

View File

@ -0,0 +1,105 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/article_detail.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/loading_indicator.dart';
import 'package:html2md/html2md.dart' as html2md;
class ArticleDetailScreen extends ConsumerWidget {
final String articleId;
const ArticleDetailScreen({super.key, required this.articleId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final articleAsync = ref.watch(articleDetailProvider(articleId));
return AppScaffold(
body: articleAsync.when(
data:
(article) => AppScaffold(
appBar: AppBar(
leading: const BackButton(),
title: Text(article.title),
),
body: _ArticleDetailContent(article: article),
),
loading: () => const Center(child: LoadingIndicator()),
error:
(error, stackTrace) =>
Center(child: Text('Failed to load article: $error')),
),
);
}
}
class _ArticleDetailContent extends HookConsumerWidget {
final SnWebArticle article;
const _ArticleDetailContent({required this.article});
@override
Widget build(BuildContext context, WidgetRef ref) {
final markdownContent = useMemoized(
() => html2md.convert(article.content ?? ''),
[article],
);
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (article.preview?.imageUrl != null)
Image.network(
article.preview!.imageUrl!,
width: double.infinity,
height: 200,
fit: BoxFit.cover,
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.title,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
if (article.feed?.title != null)
Text(
article.feed!.title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Divider(height: 32),
if (article.content != null)
...MarkdownTextContent.buildGenerator(
isDark: Theme.of(context).brightness == Brightness.dark,
).buildWidgets(markdownContent)
else if (article.preview?.description != null)
Text(article.preview!.description!),
const Gap(24),
FilledButton(
onPressed:
() => launchUrlString(
article.url,
mode: LaunchMode.externalApplication,
),
child: const Text('Read Full Article'),
),
Gap(MediaQuery.of(context).padding.bottom),
],
),
),
],
),
);
}
}

View File

@ -669,24 +669,46 @@ class EditChatScreen extends HookConsumerWidget {
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: const InputDecoration(labelText: 'Description'),
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
CheckboxListTile(
title: const Text('isPublic').tr(),
subtitle: const Text('isPublicHint').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value ?? false,
),
CheckboxListTile(
title: const Text('isCommunity').tr(),
subtitle: const Text('isCommunityHint').tr(),
value: isCommunity.value,
onChanged: (value) => isCommunity.value = value ?? false,
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
CheckboxListTile(
secondary: const Icon(Symbols.public),
title: Text('publicChat').tr(),
subtitle: Text('publicChatDescription').tr(),
value: isPublic.value,
onChanged: (value) {
isPublic.value = value ?? true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
CheckboxListTile(
secondary: const Icon(Symbols.travel_explore),
title: Text('communityChat').tr(),
subtitle: Text('communityChatDescription').tr(),
value: isCommunity.value,
onChanged: (value) {
isCommunity.value = value ?? false;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Align(

View File

@ -1,3 +1,4 @@
import 'package:dio/dio.dart';
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@ -6,14 +7,20 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/services/text.dart';
import 'package:island/widgets/account/account_picker.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'hub.g.dart';
@ -26,6 +33,73 @@ Future<SnPublisherStats?> publisherStats(Ref ref, String? uname) async {
return SnPublisherStats.fromJson(resp.data);
}
@riverpod
Future<SnPublisherMember?> publisherIdentity(Ref ref, String uname) async {
try {
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/publishers/$uname/members/me');
return SnPublisherMember.fromJson(response.data);
} catch (err) {
if (err is DioException && err.response?.statusCode == 404) {
return null; // No identity found, user is not a member
}
rethrow;
}
}
@riverpod
Future<Map<String, bool>> publisherFeatures(Ref ref, String? uname) async {
if (uname == null) return {};
final apiClient = ref.watch(apiClientProvider);
final response = await apiClient.get('/publishers/$uname/features');
return Map<String, bool>.from(response.data);
}
@riverpod
Future<List<SnPublisherMember>> publisherInvites(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/publishers/invites');
return resp.data
.map((e) => SnPublisherMember.fromJson(e))
.cast<SnPublisherMember>()
.toList();
}
@riverpod
class PublisherMemberListNotifier extends _$PublisherMemberListNotifier
with CursorPagingNotifierMixin<SnPublisherMember> {
static const int _pageSize = 20;
@override
Future<CursorPagingData<SnPublisherMember>> build(String uname) async {
return fetch();
}
@override
Future<CursorPagingData<SnPublisherMember>> fetch({String? cursor}) async {
final apiClient = ref.read(apiClientProvider);
final offset = cursor != null ? int.parse(cursor) : 0;
final response = await apiClient.get(
'/publishers/$uname/members',
queryParameters: {'offset': offset, 'take': _pageSize},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnPublisherMember.fromJson(e)).toList();
final hasMore = offset + members.length < total;
final nextCursor = hasMore ? (offset + members.length).toString() : null;
return CursorPagingData(
items: members,
hasMore: hasMore,
nextCursor: nextCursor,
);
}
}
class CreatorHubShellScreen extends StatelessWidget {
final Widget child;
const CreatorHubShellScreen({super.key, required this.child});
@ -58,21 +132,20 @@ class CreatorHubScreen extends HookConsumerWidget {
}
final publishers = ref.watch(publishersManagedProvider);
final publisherInvites = ref.watch(publisherInvitesProvider);
final currentPublisher = useState<SnPublisher?>(
publishers.value?.firstOrNull,
);
void updatePublisher() {
context
.push('/creators/${currentPublisher.value!.name}/edit')
.then((value) async {
if (value == null) return;
final data = await ref.refresh(publishersManagedProvider.future);
currentPublisher.value =
data
.where((e) => e.id == currentPublisher.value!.id)
.firstOrNull;
});
context.push('/creators/${currentPublisher.value!.name}/edit').then((
value,
) async {
if (value == null) return;
final data = await ref.refresh(publishersManagedProvider.future);
currentPublisher.value =
data.where((e) => e.id == currentPublisher.value!.id).firstOrNull;
});
}
void deletePublisher() {
@ -120,12 +193,40 @@ class CreatorHubScreen extends HookConsumerWidget {
publisherStatsProvider(currentPublisher.value?.name),
);
final publisherFeatures = ref.watch(
publisherFeaturesProvider(currentPublisher.value?.name),
);
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: !isWide ? const PageBackButton() : null,
title: Text('creatorHub').tr(),
actions: [
IconButton(
icon: Badge(
label: Text(
publisherInvites.when(
data: (invites) => invites.length.toString(),
error: (_, _) => '0',
loading: () => '0',
),
),
isLabelVisible: publisherInvites.when(
data: (invites) => invites.isNotEmpty,
error: (_, _) => false,
loading: () => false,
),
child: const Icon(Symbols.email),
),
onPressed: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (_) => const _PublisherInviteSheet(),
);
},
),
DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>(
alignment: Alignment.centerRight,
@ -203,7 +304,7 @@ class CreatorHubScreen extends HookConsumerWidget {
...(publishers.value?.map(
(publisher) => ListTile(
leading: ProfilePictureWidget(
fileId: publisher.picture?.id,
file: publisher.picture,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
@ -266,6 +367,86 @@ class CreatorHubScreen extends HookConsumerWidget {
);
},
),
ListTile(
minTileHeight: 48,
title: Text('publisherMembers').tr(),
trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.group),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _PublisherMemberListSheet(
publisherUname:
currentPublisher.value!.name,
),
);
},
),
ListTile(
minTileHeight: 48,
title: const Text('Web Feeds').tr(),
trailing: const Icon(Symbols.chevron_right),
leading: const Icon(Symbols.rss_feed),
contentPadding: const EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
context.push(
'/creators/${currentPublisher.value!.name}/feeds',
);
},
),
ExpansionTile(
title: Text('publisherFeatures').tr(),
leading: const Icon(Symbols.flag),
tilePadding: EdgeInsets.symmetric(horizontal: 24),
minTileHeight: 48,
children: [
...publisherFeatures.when(
data: (data) {
return data.entries.map((entry) {
final keyPrefix =
'publisherFeature${entry.key.capitalizeEachWord()}';
return ListTile(
minTileHeight: 48,
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
leading: Icon(
Symbols.circle,
color:
entry.value
? Colors.green
: Colors.red,
fill: 1,
size: 16,
).padding(left: 2, top: 4),
title: Text(keyPrefix).tr(),
subtitle: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text('${keyPrefix}Description').tr(),
if (!entry.value)
Text(
'${keyPrefix}Hint',
).tr().bold(),
],
),
isThreeLine: true,
);
}).toList();
},
error: (_, _) => [],
loading: () => [],
),
],
),
Divider(height: 1).padding(vertical: 8),
ListTile(
minTileHeight: 48,
@ -393,3 +574,482 @@ class _PublisherStatsWidget extends StatelessWidget {
);
}
}
class PublisherMemberState {
final List<SnPublisherMember> members;
final bool isLoading;
final int total;
final String? error;
const PublisherMemberState({
required this.members,
required this.isLoading,
required this.total,
this.error,
});
PublisherMemberState copyWith({
List<SnPublisherMember>? members,
bool? isLoading,
int? total,
String? error,
}) {
return PublisherMemberState(
members: members ?? this.members,
isLoading: isLoading ?? this.isLoading,
total: total ?? this.total,
error: error ?? this.error,
);
}
}
final publisherMemberStateProvider = StateNotifierProvider.family<
PublisherMemberNotifier,
PublisherMemberState,
String
>((ref, publisherUname) {
final apiClient = ref.watch(apiClientProvider);
return PublisherMemberNotifier(apiClient, publisherUname);
});
class PublisherMemberNotifier extends StateNotifier<PublisherMemberState> {
final String publisherUname;
final Dio _apiClient;
PublisherMemberNotifier(this._apiClient, this.publisherUname)
: super(
const PublisherMemberState(members: [], isLoading: false, total: 0),
);
Future<void> loadMore({int offset = 0, int take = 20}) async {
if (state.isLoading) return;
if (state.total > 0 && state.members.length >= state.total) return;
state = state.copyWith(isLoading: true, error: null);
try {
final response = await _apiClient.get(
'/publishers/$publisherUname/members',
queryParameters: {'offset': offset, 'take': take},
);
final total = int.parse(response.headers.value('X-Total') ?? '0');
final List<dynamic> data = response.data;
final members = data.map((e) => SnPublisherMember.fromJson(e)).toList();
state = state.copyWith(
members: [...state.members, ...members],
total: total,
isLoading: false,
);
} catch (e) {
state = state.copyWith(error: e.toString(), isLoading: false);
}
}
void reset() {
state = const PublisherMemberState(members: [], isLoading: false, total: 0);
}
}
class _PublisherMemberListSheet extends HookConsumerWidget {
final String publisherUname;
const _PublisherMemberListSheet({required this.publisherUname});
@override
Widget build(BuildContext context, WidgetRef ref) {
final publisherIdentity = ref.watch(
publisherIdentityProvider(publisherUname),
);
final memberListProvider = publisherMemberListNotifierProvider(
publisherUname,
);
final memberState = ref.watch(publisherMemberStateProvider(publisherUname));
final memberNotifier = ref.read(
publisherMemberStateProvider(publisherUname).notifier,
);
useEffect(() {
Future(() {
memberNotifier.loadMore();
});
return null;
}, []);
Future<void> invitePerson() async {
final result = await showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => const AccountPickerSheet(),
);
if (result == null) return;
try {
final apiClient = ref.watch(apiClientProvider);
await apiClient.post(
'/publishers/$publisherUname/invites',
data: {'related_user_id': result.id, 'role': 0},
);
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
}
return Container(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.8,
),
child: Column(
children: [
Padding(
padding: EdgeInsets.only(top: 16, left: 20, right: 16, bottom: 12),
child: Row(
children: [
Text(
'members'.plural(memberState.total),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.person_add),
onPressed: invitePerson,
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
IconButton(
icon: const Icon(Symbols.refresh),
onPressed: () {
memberNotifier.reset();
memberNotifier.loadMore();
ref.invalidate(memberListProvider);
},
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
),
],
),
),
const Divider(height: 1),
Expanded(
child: PagingHelperView(
provider: memberListProvider,
futureRefreshable: memberListProvider.future,
notifierRefreshable: memberListProvider.notifier,
contentBuilder: (data, widgetCount, endItemView) {
return ListView.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == data.items.length) {
return endItemView;
}
final member = data.items[index];
return ListTile(
contentPadding: EdgeInsets.only(left: 16, right: 12),
leading: ProfilePictureWidget(
fileId: member.account!.profile.picture?.id,
),
title: Row(
spacing: 6,
children: [
Flexible(child: Text(member.account!.nick)),
if (member.joinedAt == null)
const Icon(Symbols.pending_actions, size: 20),
],
),
subtitle: Row(
children: [
Text(
member.role >= 100
? 'permissionOwner'
: member.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
Text('·').bold().padding(horizontal: 6),
Expanded(child: Text("@${member.account!.name}")),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.edit),
onPressed: () {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder:
(context) => _PublisherMemberRoleSheet(
publisherUname: publisherUname,
member: member,
),
).then((value) {
if (value != null) {
ref.invalidate(memberListProvider);
}
});
},
),
if ((publisherIdentity.value?.role ?? 0) >= 50)
IconButton(
icon: const Icon(Symbols.delete),
onPressed: () {
showConfirmAlert(
'removePublisherMemberHint'.tr(),
'removePublisherMember'.tr(),
).then((confirm) async {
if (confirm != true) return;
try {
final apiClient = ref.watch(
apiClientProvider,
);
await apiClient.delete(
'/publishers/$publisherUname/members/${member.accountId}',
);
ref.invalidate(memberListProvider);
} catch (err) {
showErrorAlert(err);
}
});
},
),
],
),
);
},
);
},
),
),
],
),
);
}
}
class _PublisherMemberRoleSheet extends HookConsumerWidget {
final String publisherUname;
final SnPublisherMember member;
const _PublisherMemberRoleSheet({
required this.publisherUname,
required this.member,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final roleController = useTextEditingController(
text: member.role.toString(),
);
return Container(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.only(
top: 16,
left: 20,
right: 16,
bottom: 12,
),
child: Row(
children: [
Text(
'memberRoleEdit'.tr(args: [member.account!.name]),
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: -0.5,
),
),
const Spacer(),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => Navigator.pop(context),
style: IconButton.styleFrom(
minimumSize: const Size(36, 36),
),
),
],
),
),
const Divider(height: 1),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Autocomplete<int>(
optionsBuilder: (TextEditingValue textEditingValue) {
if (textEditingValue.text.isEmpty) {
return const [100, 50, 0];
}
final int? value = int.tryParse(textEditingValue.text);
if (value == null) return const [100, 50, 0];
return [100, 50, 0].where(
(option) =>
option.toString().contains(textEditingValue.text),
);
},
onSelected: (int selection) {
roleController.text = selection.toString();
},
fieldViewBuilder: (
context,
controller,
focusNode,
onFieldSubmitted,
) {
return TextField(
controller: controller,
focusNode: focusNode,
keyboardType: TextInputType.number,
decoration: InputDecoration(
labelText: 'memberRole'.tr(),
helperText: 'memberRoleHint'.tr(),
),
onTapOutside: (event) => focusNode.unfocus(),
);
},
),
const Gap(16),
FilledButton.icon(
onPressed: () async {
try {
final newRole = int.parse(roleController.text);
if (newRole < 0 || newRole > 100) {
throw 'Role must be between 0 and 100';
}
final apiClient = ref.read(apiClientProvider);
await apiClient.patch(
'/publishers/$publisherUname/members/${member.accountId}/role',
data: newRole,
);
if (context.mounted) Navigator.pop(context, true);
} catch (err) {
showErrorAlert(err);
}
},
icon: const Icon(Symbols.save),
label: const Text('saveChanges').tr(),
),
],
).padding(vertical: 16, horizontal: 24),
],
),
),
);
}
}
class _PublisherInviteSheet extends HookConsumerWidget {
const _PublisherInviteSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final invites = ref.watch(publisherInvitesProvider);
Future<void> acceptInvite(SnPublisherMember invite) async {
try {
final client = ref.read(apiClientProvider);
await client.post(
'/publishers/invites/${invite.publisher!.name}/accept',
);
ref.invalidate(publisherInvitesProvider);
ref.invalidate(publishersManagedProvider);
} catch (err) {
showErrorAlert(err);
}
}
Future<void> declineInvite(SnPublisherMember invite) async {
try {
final client = ref.read(apiClientProvider);
await client.post(
'/publishers/invites/${invite.publisher!.name}/decline',
);
ref.invalidate(publisherInvitesProvider);
} catch (err) {
showErrorAlert(err);
}
}
return SheetScaffold(
titleText: 'invites'.tr(),
actions: [
IconButton(
icon: const Icon(Symbols.refresh),
style: IconButton.styleFrom(minimumSize: const Size(36, 36)),
onPressed: () {
ref.invalidate(publisherInvitesProvider);
},
),
],
child: invites.when(
data:
(items) =>
items.isEmpty
? Center(
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.publisher!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.publisher!.nick),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(publisherInvitesProvider),
),
),
);
}
}

View File

@ -149,5 +149,422 @@ class _PublisherStatsProviderElement
String? get uname => (origin as PublisherStatsProvider).uname;
}
String _$publisherIdentityHash() => r'f7fd986a303a729ca5557022fceb37cd01fa17f3';
/// See also [publisherIdentity].
@ProviderFor(publisherIdentity)
const publisherIdentityProvider = PublisherIdentityFamily();
/// See also [publisherIdentity].
class PublisherIdentityFamily extends Family<AsyncValue<SnPublisherMember?>> {
/// See also [publisherIdentity].
const PublisherIdentityFamily();
/// See also [publisherIdentity].
PublisherIdentityProvider call(String uname) {
return PublisherIdentityProvider(uname);
}
@override
PublisherIdentityProvider getProviderOverride(
covariant PublisherIdentityProvider provider,
) {
return call(provider.uname);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'publisherIdentityProvider';
}
/// See also [publisherIdentity].
class PublisherIdentityProvider
extends AutoDisposeFutureProvider<SnPublisherMember?> {
/// See also [publisherIdentity].
PublisherIdentityProvider(String uname)
: this._internal(
(ref) => publisherIdentity(ref as PublisherIdentityRef, uname),
from: publisherIdentityProvider,
name: r'publisherIdentityProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherIdentityHash,
dependencies: PublisherIdentityFamily._dependencies,
allTransitiveDependencies:
PublisherIdentityFamily._allTransitiveDependencies,
uname: uname,
);
PublisherIdentityProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
Override overrideWith(
FutureOr<SnPublisherMember?> Function(PublisherIdentityRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PublisherIdentityProvider._internal(
(ref) => create(ref as PublisherIdentityRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<SnPublisherMember?> createElement() {
return _PublisherIdentityProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherIdentityProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PublisherIdentityRef on AutoDisposeFutureProviderRef<SnPublisherMember?> {
/// The parameter `uname` of this provider.
String get uname;
}
class _PublisherIdentityProviderElement
extends AutoDisposeFutureProviderElement<SnPublisherMember?>
with PublisherIdentityRef {
_PublisherIdentityProviderElement(super.provider);
@override
String get uname => (origin as PublisherIdentityProvider).uname;
}
String _$publisherFeaturesHash() => r'34db65d9a4b6b0c6961733ae79e67f25d5d111d3';
/// See also [publisherFeatures].
@ProviderFor(publisherFeatures)
const publisherFeaturesProvider = PublisherFeaturesFamily();
/// See also [publisherFeatures].
class PublisherFeaturesFamily extends Family<AsyncValue<Map<String, bool>>> {
/// See also [publisherFeatures].
const PublisherFeaturesFamily();
/// See also [publisherFeatures].
PublisherFeaturesProvider call(String? uname) {
return PublisherFeaturesProvider(uname);
}
@override
PublisherFeaturesProvider getProviderOverride(
covariant PublisherFeaturesProvider provider,
) {
return call(provider.uname);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'publisherFeaturesProvider';
}
/// See also [publisherFeatures].
class PublisherFeaturesProvider
extends AutoDisposeFutureProvider<Map<String, bool>> {
/// See also [publisherFeatures].
PublisherFeaturesProvider(String? uname)
: this._internal(
(ref) => publisherFeatures(ref as PublisherFeaturesRef, uname),
from: publisherFeaturesProvider,
name: r'publisherFeaturesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherFeaturesHash,
dependencies: PublisherFeaturesFamily._dependencies,
allTransitiveDependencies:
PublisherFeaturesFamily._allTransitiveDependencies,
uname: uname,
);
PublisherFeaturesProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String? uname;
@override
Override overrideWith(
FutureOr<Map<String, bool>> Function(PublisherFeaturesRef provider) create,
) {
return ProviderOverride(
origin: this,
override: PublisherFeaturesProvider._internal(
(ref) => create(ref as PublisherFeaturesRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<Map<String, bool>> createElement() {
return _PublisherFeaturesProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherFeaturesProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PublisherFeaturesRef on AutoDisposeFutureProviderRef<Map<String, bool>> {
/// The parameter `uname` of this provider.
String? get uname;
}
class _PublisherFeaturesProviderElement
extends AutoDisposeFutureProviderElement<Map<String, bool>>
with PublisherFeaturesRef {
_PublisherFeaturesProviderElement(super.provider);
@override
String? get uname => (origin as PublisherFeaturesProvider).uname;
}
String _$publisherInvitesHash() => r'488cd443407895ce11f4edff07cb6ea58f2aa018';
/// See also [publisherInvites].
@ProviderFor(publisherInvites)
final publisherInvitesProvider =
AutoDisposeFutureProvider<List<SnPublisherMember>>.internal(
publisherInvites,
name: r'publisherInvitesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherInvitesHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef PublisherInvitesRef =
AutoDisposeFutureProviderRef<List<SnPublisherMember>>;
String _$publisherMemberListNotifierHash() =>
r'237e8f39c9757a6cbdff817853c697539242ad2a';
abstract class _$PublisherMemberListNotifier
extends
BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnPublisherMember>> {
late final String uname;
FutureOr<CursorPagingData<SnPublisherMember>> build(String uname);
}
/// See also [PublisherMemberListNotifier].
@ProviderFor(PublisherMemberListNotifier)
const publisherMemberListNotifierProvider = PublisherMemberListNotifierFamily();
/// See also [PublisherMemberListNotifier].
class PublisherMemberListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnPublisherMember>>> {
/// See also [PublisherMemberListNotifier].
const PublisherMemberListNotifierFamily();
/// See also [PublisherMemberListNotifier].
PublisherMemberListNotifierProvider call(String uname) {
return PublisherMemberListNotifierProvider(uname);
}
@override
PublisherMemberListNotifierProvider getProviderOverride(
covariant PublisherMemberListNotifierProvider provider,
) {
return call(provider.uname);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'publisherMemberListNotifierProvider';
}
/// See also [PublisherMemberListNotifier].
class PublisherMemberListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
> {
/// See also [PublisherMemberListNotifier].
PublisherMemberListNotifierProvider(String uname)
: this._internal(
() => PublisherMemberListNotifier()..uname = uname,
from: publisherMemberListNotifierProvider,
name: r'publisherMemberListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$publisherMemberListNotifierHash,
dependencies: PublisherMemberListNotifierFamily._dependencies,
allTransitiveDependencies:
PublisherMemberListNotifierFamily._allTransitiveDependencies,
uname: uname,
);
PublisherMemberListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String uname;
@override
FutureOr<CursorPagingData<SnPublisherMember>> runNotifierBuild(
covariant PublisherMemberListNotifier notifier,
) {
return notifier.build(uname);
}
@override
Override overrideWith(PublisherMemberListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: PublisherMemberListNotifierProvider._internal(
() => create()..uname = uname,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
>
createElement() {
return _PublisherMemberListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is PublisherMemberListNotifierProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin PublisherMemberListNotifierRef
on
AutoDisposeAsyncNotifierProviderRef<
CursorPagingData<SnPublisherMember>
> {
/// The parameter `uname` of this provider.
String get uname;
}
class _PublisherMemberListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
PublisherMemberListNotifier,
CursorPagingData<SnPublisherMember>
>
with PublisherMemberListNotifierRef {
_PublisherMemberListNotifierProviderElement(super.provider);
@override
String get uname => (origin as PublisherMemberListNotifierProvider).uname;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -8,7 +8,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
@ -270,7 +270,10 @@ class EditPublisherScreen extends HookConsumerWidget {
),
TextFormField(
controller: bioController,
decoration: InputDecoration(labelText: 'bio'.tr()),
decoration: InputDecoration(
labelText: 'bio'.tr(),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:

View File

@ -71,9 +71,7 @@ class SliverStickerPacksList extends HookConsumerWidget {
subtitle: Text(sticker.description),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.push(
'/creators/$pubName/stickers/${sticker.id}',
);
context.push('/creators/$pubName/stickers/${sticker.id}');
},
);
},
@ -230,6 +228,7 @@ class EditStickerPacksScreen extends HookConsumerWidget {
decoration: InputDecoration(
labelText: 'description'.tr(),
border: const UnderlineInputBorder(),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,

View File

@ -0,0 +1,287 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/webfeed.dart';
import 'package:island/widgets/alert.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:styled_widget/styled_widget.dart';
class WebFeedNewScreen extends StatelessWidget {
final String pubName;
const WebFeedNewScreen({super.key, required this.pubName});
@override
Widget build(BuildContext context) {
return WebFeedEditScreen(pubName: pubName, feedId: null);
}
}
class WebFeedEditScreen extends HookConsumerWidget {
final String pubName;
final String? feedId;
const WebFeedEditScreen({super.key, required this.pubName, this.feedId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final formKey = useMemoized(() => GlobalKey<FormState>());
final titleController = useTextEditingController();
final urlController = useTextEditingController();
final descriptionController = useTextEditingController();
final isLoading = useState(false);
final isScrapEnabled = useState(false);
final saveFeed = useCallback(() async {
if (!formKey.currentState!.validate()) return;
isLoading.value = true;
try {
final feed = SnWebFeed(
id: feedId ?? '',
title: titleController.text,
url: urlController.text,
description: descriptionController.text,
config: SnWebFeedConfig(scrapPage: isScrapEnabled.value),
publisherId: pubName,
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
deletedAt: null,
);
await ref
.read(
webFeedNotifierProvider((
pubName: pubName,
feedId: feedId,
)).notifier,
)
.saveFeed(feed);
// Refresh the feed list
ref.invalidate(webFeedListProvider(pubName));
if (context.mounted) {
showSnackBar('Web feed saved successfully');
context.pop();
}
} catch (e) {
showErrorAlert(e);
} finally {
isLoading.value = false;
}
}, [pubName, feedId, isScrapEnabled.value, context]);
final deleteFeed = useCallback(() async {
final confirmed = await showConfirmAlert(
'Are you sure you want to delete this web feed? This action cannot be undone.',
'Delete Web Feed',
);
if (confirmed != true) return;
isLoading.value = true;
try {
await ref
.read(
webFeedNotifierProvider((
pubName: pubName,
feedId: feedId!,
)).notifier,
)
.deleteFeed();
ref.invalidate(webFeedListProvider(pubName));
if (context.mounted) {
showSnackBar('Web feed deleted successfully');
context.pop();
}
} catch (e) {
showErrorAlert(e);
} finally {
isLoading.value = false;
}
}, [pubName, feedId, context, ref]);
final feedAsync = ref.watch(
webFeedNotifierProvider((pubName: pubName, feedId: feedId)),
);
return feedAsync.when(
loading:
() =>
const Scaffold(body: Center(child: CircularProgressIndicator())),
error:
(error, stack) => Scaffold(
appBar: AppBar(title: const Text('Error')),
body: Center(child: Text('Error: $error')),
),
data: (feed) {
// Initialize form fields if they're empty and we have a feed
if (titleController.text.isEmpty) {
titleController.text = feed.title;
urlController.text = feed.url;
descriptionController.text = feed.description ?? '';
isScrapEnabled.value = feed.config.scrapPage;
}
return _buildForm(
context,
formKey: formKey,
titleController: titleController,
urlController: urlController,
descriptionController: descriptionController,
isScrapEnabled: isScrapEnabled.value,
onScrapEnabledChanged: (value) => isScrapEnabled.value = value,
onSave: saveFeed,
onDelete: deleteFeed,
isLoading: isLoading.value,
ref: ref,
hasFeedId: feedId != null,
);
},
);
}
Widget _buildForm(
BuildContext context, {
required WidgetRef ref,
required GlobalKey<FormState> formKey,
required TextEditingController titleController,
required TextEditingController urlController,
required TextEditingController descriptionController,
required bool isScrapEnabled,
required ValueChanged<bool> onScrapEnabledChanged,
required VoidCallback onSave,
required VoidCallback onDelete,
required bool isLoading,
required bool hasFeedId,
}) {
final scrapNow = useCallback(() async {
showLoadingModal(context);
try {
await ref
.read(
webFeedNotifierProvider((
pubName: pubName,
feedId: feedId!,
)).notifier,
)
.scrapFeed();
if (context.mounted) {
showSnackBar('Feed scraping successfully.');
}
} catch (e) {
showErrorAlert(e);
} finally {
if (context.mounted) hideLoadingModal(context);
}
}, [pubName, feedId, ref, context]);
return Scaffold(
appBar: AppBar(
title: Text(hasFeedId ? 'Edit Web Feed' : 'New Web Feed'),
actions: [
if (hasFeedId)
IconButton(
icon: const Icon(Symbols.delete_forever),
onPressed: isLoading ? null : onDelete,
),
const SizedBox(width: 8),
],
),
body: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
children: [
TextFormField(
controller: titleController,
decoration: const InputDecoration(labelText: 'Title'),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a title';
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: urlController,
decoration: const InputDecoration(
labelText: 'URL',
hintText: 'https://example.com/feed',
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter a URL';
}
final uri = Uri.tryParse(value);
if (uri == null || !uri.hasAbsolutePath) {
return 'Please enter a valid URL';
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: const InputDecoration(
labelText: 'Description',
alignLabelWithHint: true,
),
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
maxLines: 3,
),
const SizedBox(height: 24),
Card(
margin: EdgeInsets.zero,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: const Text('Scrape web page for content'),
subtitle: const Text(
'When enabled, the system will attempt to extract full content from the web page',
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
value: isScrapEnabled,
onChanged: onScrapEnabledChanged,
),
],
),
),
const SizedBox(height: 20),
if (hasFeedId) ...[
FilledButton.tonalIcon(
onPressed: isLoading ? null : scrapNow,
icon: const Icon(Symbols.refresh),
label: const Text('Scrape Now'),
).alignment(Alignment.centerRight),
const SizedBox(height: 16),
],
FilledButton.icon(
onPressed: isLoading ? null : onSave,
icon: const Icon(Symbols.save),
label: Text('saveChanges').tr(),
).alignment(Alignment.centerRight),
],
).padding(all: 20),
),
),
);
}
}

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:island/pods/webfeed.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/empty_state.dart';
import 'package:material_symbols_icons/symbols.dart';
class WebFeedListScreen extends ConsumerWidget {
final String pubName;
const WebFeedListScreen({super.key, required this.pubName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final feedsAsync = ref.watch(webFeedListProvider(pubName));
return AppScaffold(
appBar: AppBar(title: const Text('Web Feeds')),
floatingActionButton: FloatingActionButton(
child: const Icon(Symbols.add),
onPressed: () {
context.push('/creators/$pubName/feeds/new');
},
),
body: feedsAsync.when(
data: (feeds) {
if (feeds.isEmpty) {
return EmptyState(
icon: Symbols.rss_feed,
title: 'No Web Feeds',
description: 'Add a new web feed to get started',
);
}
return RefreshIndicator(
onRefresh: () => ref.refresh(webFeedListProvider(pubName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 8),
itemCount: feeds.length,
itemBuilder: (context, index) {
final feed = feeds[index];
return Card(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
child: ListTile(
leading: const Icon(Symbols.rss_feed, size: 32),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
title: Text(
feed.title,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
feed.url,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
context.push('/creators/$pubName/feeds/${feed.id}');
},
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, _) => Center(child: Text('Error: $error')),
),
);
}
}

View File

@ -0,0 +1,162 @@
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/custom_app.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'apps.g.dart';
@riverpod
Future<List<CustomApp>> customApps(Ref ref, String publisherName) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers/$publisherName/apps');
return resp.data.map((e) => CustomApp.fromJson(e)).cast<CustomApp>().toList();
}
class CustomAppsScreen extends HookConsumerWidget {
final String publisherName;
const CustomAppsScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apps = ref.watch(customAppsProvider(publisherName));
return AppScaffold(
appBar: AppBar(
title: Text('customApps').tr(),
actions: [
IconButton(
icon: const Icon(Symbols.add),
onPressed: () {
context.push('/developers/$publisherName/apps/new');
},
),
],
),
body: apps.when(
data: (data) {
if (data.isEmpty) {
return Center(child: Text('noCustomApps').tr());
}
return RefreshIndicator(
onRefresh:
() => ref.refresh(customAppsProvider(publisherName).future),
child: ListView.builder(
padding: EdgeInsets.only(top: 4),
itemCount: data.length,
itemBuilder: (context, index) {
final app = data[index];
return Card(
margin: const EdgeInsets.all(8.0),
child: Column(
children: [
SizedBox(
height: 150,
child: Stack(
fit: StackFit.expand,
children: [
if (app.background != null)
CloudFileWidget(
item: app.background!,
fit: BoxFit.cover,
).clipRRect(topLeft: 8, topRight: 8),
if (app.picture != null)
Positioned(
left: 16,
bottom: 16,
child: ProfilePictureWidget(
fileId: app.picture!.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
),
],
),
),
ListTile(
title: Text(app.name),
subtitle: Text(
app.slug,
style: GoogleFonts.robotoMono(fontSize: 12),
),
contentPadding: EdgeInsets.only(left: 20, right: 12),
trailing: PopupMenuButton(
itemBuilder:
(context) => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Symbols.edit),
const SizedBox(width: 12),
Text('edit').tr(),
],
),
),
PopupMenuItem(
value: 'delete',
child: Row(
children: [
const Icon(
Symbols.delete,
color: Colors.red,
),
const SizedBox(width: 12),
Text(
'delete',
style: TextStyle(color: Colors.red),
).tr(),
],
),
),
],
onSelected: (value) {
if (value == 'edit') {
context.push(
'/developers/$publisherName/apps/${app.id}',
);
} else if (value == 'delete') {
showConfirmAlert(
'deleteCustomAppHint'.tr(),
'deleteCustomApp'.tr(),
).then((confirm) {
if (confirm) {
final client = ref.read(apiClientProvider);
client.delete(
'/developers/$publisherName/apps/${app.id}',
);
ref.invalidate(
customAppsProvider(publisherName),
);
}
});
}
},
),
),
],
),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () => ref.invalidate(customAppsProvider(publisherName)),
),
),
);
}
}

View File

@ -0,0 +1,151 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'apps.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$customAppsHash() => r'1dec11573b9d987c3adbdf4732b3781a6f40172a';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [customApps].
@ProviderFor(customApps)
const customAppsProvider = CustomAppsFamily();
/// See also [customApps].
class CustomAppsFamily extends Family<AsyncValue<List<CustomApp>>> {
/// See also [customApps].
const CustomAppsFamily();
/// See also [customApps].
CustomAppsProvider call(String publisherName) {
return CustomAppsProvider(publisherName);
}
@override
CustomAppsProvider getProviderOverride(
covariant CustomAppsProvider provider,
) {
return call(provider.publisherName);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'customAppsProvider';
}
/// See also [customApps].
class CustomAppsProvider extends AutoDisposeFutureProvider<List<CustomApp>> {
/// See also [customApps].
CustomAppsProvider(String publisherName)
: this._internal(
(ref) => customApps(ref as CustomAppsRef, publisherName),
from: customAppsProvider,
name: r'customAppsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$customAppsHash,
dependencies: CustomAppsFamily._dependencies,
allTransitiveDependencies: CustomAppsFamily._allTransitiveDependencies,
publisherName: publisherName,
);
CustomAppsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
}) : super.internal();
final String publisherName;
@override
Override overrideWith(
FutureOr<List<CustomApp>> Function(CustomAppsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: CustomAppsProvider._internal(
(ref) => create(ref as CustomAppsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
),
);
}
@override
AutoDisposeFutureProviderElement<List<CustomApp>> createElement() {
return _CustomAppsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is CustomAppsProvider && other.publisherName == publisherName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin CustomAppsRef on AutoDisposeFutureProviderRef<List<CustomApp>> {
/// The parameter `publisherName` of this provider.
String get publisherName;
}
class _CustomAppsProviderElement
extends AutoDisposeFutureProviderElement<List<CustomApp>>
with CustomAppsRef {
_CustomAppsProviderElement(super.provider);
@override
String get publisherName => (origin as CustomAppsProvider).publisherName;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -0,0 +1,558 @@
import 'package:croppy/croppy.dart' hide cropImage;
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/custom_app.dart';
import 'package:island/models/file.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/developers/apps.dart';
import 'package:island/services/file.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:island/widgets/content/sheet.dart';
part 'edit_app.g.dart';
@riverpod
Future<CustomApp?> customApp(Ref ref, String publisherName, String id) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers/$publisherName/apps/$id');
return CustomApp.fromJson(resp.data);
}
class EditAppScreen extends HookConsumerWidget {
final String publisherName;
final String? id;
const EditAppScreen({super.key, required this.publisherName, this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isNew = id == null;
final app = isNew ? null : ref.watch(customAppProvider(publisherName, id!));
final formKey = useMemoized(() => GlobalKey<FormState>());
final submitting = useState(false);
final nameController = useTextEditingController();
final slugController = useTextEditingController();
final descriptionController = useTextEditingController();
final picture = useState<SnCloudFile?>(null);
final background = useState<SnCloudFile?>(null);
final enableLinks = useState(false); // Only for UI purposes
final homePageController = useTextEditingController();
final privacyPolicyController = useTextEditingController();
final termsController = useTextEditingController();
final oauthEnabled = useState(false);
final redirectUris = useState<List<String>>([]);
final postLogoutUris = useState<List<String>>([]);
final allowedScopes = useState<List<String>>([
'openid',
'profile',
'email',
]);
final allowedGrantTypes = useState<List<String>>([
'authorization_code',
'refresh_token',
]);
final requirePkce = useState(true);
final allowOfflineAccess = useState(false);
useEffect(() {
if (app?.value != null) {
nameController.text = app!.value!.name;
slugController.text = app.value!.slug;
descriptionController.text = app.value!.description ?? '';
picture.value = app.value!.picture;
background.value = app.value!.background;
homePageController.text = app.value!.links?.homePage ?? '';
privacyPolicyController.text = app.value!.links?.privacyPolicy ?? '';
termsController.text = app.value!.links?.termsOfService ?? '';
if (app.value!.oauthConfig != null) {
oauthEnabled.value = true;
redirectUris.value = app.value!.oauthConfig!.redirectUris;
postLogoutUris.value =
app.value!.oauthConfig!.postLogoutRedirectUris ?? [];
allowedScopes.value = app.value!.oauthConfig!.allowedScopes;
allowedGrantTypes.value = app.value!.oauthConfig!.allowedGrantTypes;
requirePkce.value = app.value!.oauthConfig!.requirePkce;
allowOfflineAccess.value = app.value!.oauthConfig!.allowOfflineAccess;
}
}
return null;
}, [app]);
void setPicture(String position) async {
showLoadingModal(context);
var result = await ref
.read(imagePickerProvider)
.pickImage(source: ImageSource.gallery);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
hideLoadingModal(context);
result = await cropImage(
context,
image: result,
allowedAspectRatios: [
if (position == 'background')
const CropAspectRatio(height: 7, width: 16)
else
const CropAspectRatio(height: 1, width: 1),
],
);
if (result == null) {
if (context.mounted) hideLoadingModal(context);
return;
}
if (!context.mounted) return;
showLoadingModal(context);
submitting.value = true;
try {
final baseUrl = ref.watch(serverUrlProvider);
final token = await getToken(ref.watch(tokenProvider));
if (token == null) throw ArgumentError('Token is null');
final cloudFile =
await putMediaToCloud(
fileData: UniversalFile(
data: result,
type: UniversalFileType.image,
),
atk: token,
baseUrl: baseUrl,
filename: result.name,
mimetype: result.mimeType ?? 'image/jpeg',
).future;
if (cloudFile == null) {
throw ArgumentError('Failed to upload the file...');
}
switch (position) {
case 'picture':
picture.value = cloudFile;
case 'background':
background.value = cloudFile;
}
} catch (err) {
showErrorAlert(err);
} finally {
if (context.mounted) hideLoadingModal(context);
submitting.value = false;
}
}
void showAddScopeDialog() {
final scopeController = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'addScope'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: scopeController,
decoration: InputDecoration(labelText: 'scopeName'.tr()),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (scopeController.text.isNotEmpty) {
allowedScopes.value = [
...allowedScopes.value,
scopeController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
);
}
void showAddRedirectUriDialog() {
final uriController = TextEditingController();
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'addRedirectUri'.tr(),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextFormField(
controller: uriController,
decoration: InputDecoration(
labelText: 'redirectUri'.tr(),
hintText: 'https://example.com/auth/callback',
helperText: 'redirectUriHint'.tr(),
helperMaxLines: 3,
),
keyboardType: TextInputType.url,
validator: (value) {
if (value == null || value.isEmpty) {
return 'uriRequired'.tr();
}
final uri = Uri.tryParse(value);
if (uri == null || !uri.hasAbsolutePath) {
return 'invalidUri'.tr();
}
return null;
},
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 20),
FilledButton.tonalIcon(
onPressed: () {
if (uriController.text.isNotEmpty) {
redirectUris.value = [
...redirectUris.value,
uriController.text,
];
Navigator.pop(context);
}
},
icon: const Icon(Symbols.add),
label: Text('add').tr(),
),
],
),
),
),
);
}
void performAction() async {
final client = ref.read(apiClientProvider);
final data = {
'name': nameController.text,
'slug': slugController.text,
'description': descriptionController.text,
'picture_id': picture.value?.id,
'background_id': background.value?.id,
'links': {
'home_page':
homePageController.text.isNotEmpty
? homePageController.text
: null,
'privacy_policy':
privacyPolicyController.text.isNotEmpty
? privacyPolicyController.text
: null,
'terms_of_service':
termsController.text.isNotEmpty ? termsController.text : null,
},
'oauth_config':
oauthEnabled.value
? {
'redirect_uris': redirectUris.value,
'post_logout_redirect_uris':
postLogoutUris.value.isNotEmpty
? postLogoutUris.value
: null,
'allowed_scopes': allowedScopes.value,
'allowed_grant_types': allowedGrantTypes.value,
'require_pkce': requirePkce.value,
'allow_offline_access': allowOfflineAccess.value,
}
: null,
};
if (isNew) {
await client.post('/developers/$publisherName/apps', data: data);
} else {
await client.patch('/developers/$publisherName/apps/$id', data: data);
}
ref.invalidate(customAppsProvider(publisherName));
if (context.mounted) {
Navigator.pop(context);
}
}
return AppScaffold(
appBar: AppBar(
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
),
body:
app == null && !isNew
? const Center(child: CircularProgressIndicator())
: app?.hasError == true && !isNew
? ResponseErrorWidget(
error: app!.error,
onRetry:
() => ref.invalidate(customAppProvider(publisherName, id!)),
)
: SingleChildScrollView(
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 7,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
GestureDetector(
child: Container(
color:
Theme.of(
context,
).colorScheme.surfaceContainerHigh,
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
setPicture('background');
},
),
Positioned(
left: 20,
bottom: -32,
child: GestureDetector(
child: ProfilePictureWidget(
fileId: picture.value?.id,
radius: 40,
fallbackIcon: Symbols.apps,
),
onTap: () {
setPicture('picture');
},
),
),
],
),
).padding(bottom: 32),
Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: nameController,
decoration: InputDecoration(labelText: 'name'.tr()),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: slugController,
decoration: InputDecoration(
labelText: 'slug'.tr(),
helperText: 'slugHint'.tr(),
),
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
maxLines: 3,
onTapOutside:
(_) =>
FocusManager.instance.primaryFocus
?.unfocus(),
),
const SizedBox(height: 16),
ExpansionPanelList(
expansionCallback: (index, isExpanded) {
switch (index) {
case 0:
enableLinks.value = isExpanded;
break;
case 1:
oauthEnabled.value = isExpanded;
break;
}
},
children: [
ExpansionPanel(
headerBuilder:
(context, isExpanded) =>
ListTile(title: Text('appLinks').tr()),
body: Column(
spacing: 16,
children: [
TextFormField(
controller: homePageController,
decoration: InputDecoration(
labelText: 'homePageUrl'.tr(),
hintText: 'https://example.com',
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: privacyPolicyController,
decoration: InputDecoration(
labelText: 'privacyPolicyUrl'.tr(),
hintText: 'https://example.com/privacy',
),
keyboardType: TextInputType.url,
),
TextFormField(
controller: termsController,
decoration: InputDecoration(
labelText: 'termsOfServiceUrl'.tr(),
hintText: 'https://example.com/terms',
),
keyboardType: TextInputType.url,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: enableLinks.value,
),
ExpansionPanel(
headerBuilder:
(context, isExpanded) => ListTile(
title: Text('oauthConfig').tr(),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('redirectUris'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...redirectUris.value.map(
(uri) => ListTile(
title: Text(uri),
trailing: IconButton(
icon: const Icon(
Symbols.delete,
),
onPressed: () {
redirectUris.value =
redirectUris.value
.where(
(u) => u != uri,
)
.toList();
},
),
),
),
if (redirectUris.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('addRedirectUri'.tr()),
onTap: showAddRedirectUriDialog,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Text('allowedScopes'.tr()),
Card(
margin: const EdgeInsets.symmetric(
vertical: 8,
),
child: Column(
children: [
...allowedScopes.value.map(
(scope) => ListTile(
title: Text(scope),
trailing: IconButton(
icon: const Icon(
Symbols.delete,
),
onPressed: () {
allowedScopes.value =
allowedScopes.value
.where(
(s) => s != scope,
)
.toList();
},
),
),
),
if (allowedScopes.value.isNotEmpty)
const Divider(height: 1),
ListTile(
leading: const Icon(Symbols.add),
title: Text('add').tr(),
onTap: showAddScopeDialog,
),
],
),
),
const SizedBox(height: 16),
SwitchListTile(
title: Text('requirePkce'.tr()),
value: requirePkce.value,
onChanged:
(value) => requirePkce.value = value,
),
SwitchListTile(
title: Text('allowOfflineAccess'.tr()),
value: allowOfflineAccess.value,
onChanged:
(value) =>
allowOfflineAccess.value = value,
),
],
).padding(horizontal: 16, bottom: 24),
isExpanded: oauthEnabled.value,
),
],
),
const SizedBox(height: 16),
Align(
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed:
submitting.value ? null : performAction,
label: Text('saveChanges'.tr()),
icon: const Icon(Symbols.save),
),
),
],
).padding(all: 24),
),
],
),
),
);
}
}

View File

@ -0,0 +1,161 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'edit_app.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$customAppHash() => r'aa4d1fb803c47a99cbacf6d91481f4fce3fda457';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [customApp].
@ProviderFor(customApp)
const customAppProvider = CustomAppFamily();
/// See also [customApp].
class CustomAppFamily extends Family<AsyncValue<CustomApp?>> {
/// See also [customApp].
const CustomAppFamily();
/// See also [customApp].
CustomAppProvider call(String publisherName, String id) {
return CustomAppProvider(publisherName, id);
}
@override
CustomAppProvider getProviderOverride(covariant CustomAppProvider provider) {
return call(provider.publisherName, provider.id);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'customAppProvider';
}
/// See also [customApp].
class CustomAppProvider extends AutoDisposeFutureProvider<CustomApp?> {
/// See also [customApp].
CustomAppProvider(String publisherName, String id)
: this._internal(
(ref) => customApp(ref as CustomAppRef, publisherName, id),
from: customAppProvider,
name: r'customAppProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$customAppHash,
dependencies: CustomAppFamily._dependencies,
allTransitiveDependencies: CustomAppFamily._allTransitiveDependencies,
publisherName: publisherName,
id: id,
);
CustomAppProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.publisherName,
required this.id,
}) : super.internal();
final String publisherName;
final String id;
@override
Override overrideWith(
FutureOr<CustomApp?> Function(CustomAppRef provider) create,
) {
return ProviderOverride(
origin: this,
override: CustomAppProvider._internal(
(ref) => create(ref as CustomAppRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
publisherName: publisherName,
id: id,
),
);
}
@override
AutoDisposeFutureProviderElement<CustomApp?> createElement() {
return _CustomAppProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is CustomAppProvider &&
other.publisherName == publisherName &&
other.id == id;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, publisherName.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin CustomAppRef on AutoDisposeFutureProviderRef<CustomApp?> {
/// The parameter `publisherName` of this provider.
String get publisherName;
/// The parameter `id` of this provider.
String get id;
}
class _CustomAppProviderElement
extends AutoDisposeFutureProviderElement<CustomApp?>
with CustomAppRef {
_CustomAppProviderElement(super.provider);
@override
String get publisherName => (origin as CustomAppProvider).publisherName;
@override
String get id => (origin as CustomAppProvider).id;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -0,0 +1,380 @@
import 'package:dropdown_button2/dropdown_button2.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/developer.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:island/widgets/response.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'hub.g.dart';
@riverpod
Future<DeveloperStats?> developerStats(Ref ref, String? uname) async {
if (uname == null) return null;
final apiClient = ref.watch(apiClientProvider);
final resp = await apiClient.get('/developers/$uname/stats');
return DeveloperStats.fromJson(resp.data);
}
@riverpod
Future<List<SnPublisher>> developers(Ref ref) async {
final client = ref.watch(apiClientProvider);
final resp = await client.get('/developers');
return resp.data
.map((e) => SnPublisher.fromJson(e))
.cast<SnPublisher>()
.toList();
}
class DeveloperHubShellScreen extends StatelessWidget {
final Widget child;
const DeveloperHubShellScreen({super.key, required this.child});
@override
Widget build(BuildContext context) {
final isWide = isWideScreen(context);
if (isWide) {
return Row(
children: [
SizedBox(width: 360, child: const DeveloperHubScreen(isAside: true)),
const VerticalDivider(width: 1),
Expanded(child: child),
],
);
}
return child;
}
}
class DeveloperHubScreen extends HookConsumerWidget {
final bool isAside;
const DeveloperHubScreen({super.key, this.isAside = false});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isWide = isWideScreen(context);
if (isWide && !isAside) {
return Container(color: Theme.of(context).colorScheme.surface);
}
final developers = ref.watch(developersProvider);
final currentDeveloper = useState<SnPublisher?>(
developers.value?.firstOrNull,
);
final List<DropdownMenuItem<SnPublisher>> developersMenu = developers.when(
data:
(data) =>
data
.map(
(item) => DropdownMenuItem<SnPublisher>(
value: item,
child: ListTile(
minTileHeight: 48,
leading: ProfilePictureWidget(
radius: 16,
fileId: item.picture?.id,
),
title: Text(item.nick),
subtitle: Text('@${item.name}'),
trailing:
currentDeveloper.value?.id == item.id
? const Icon(Icons.check)
: null,
contentPadding: EdgeInsets.symmetric(horizontal: 8),
),
),
)
.toList(),
loading: () => [],
error: (_, _) => [],
);
final developerStats = ref.watch(
developerStatsProvider(currentDeveloper.value?.name),
);
return AppScaffold(
noBackground: false,
appBar: AppBar(
leading: !isWide ? const PageBackButton() : null,
title: Text('developerHub').tr(),
actions: [
DropdownButtonHideUnderline(
child: DropdownButton2<SnPublisher>(
alignment: Alignment.centerRight,
value: currentDeveloper.value,
hint: CircleAvatar(
radius: 16,
child: Icon(
Symbols.person,
color: Theme.of(
context,
).colorScheme.onSecondaryContainer.withOpacity(0.9),
fill: 1,
),
).center().padding(right: 8),
items: [...developersMenu],
onChanged: (value) {
currentDeveloper.value = value;
},
selectedItemBuilder: (context) {
return [
...developersMenu.map(
(e) => ProfilePictureWidget(
radius: 16,
fileId: e.value?.picture?.id,
).center().padding(right: 8),
),
];
},
buttonStyleData: ButtonStyleData(
height: 40,
padding: const EdgeInsets.only(left: 14, right: 8),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
),
),
dropdownStyleData: DropdownStyleData(
width: 320,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
),
menuItemStyleData: const MenuItemStyleData(
height: 64,
padding: EdgeInsets.only(left: 14, right: 14),
),
iconStyleData: IconStyleData(
icon: Icon(Icons.arrow_drop_down),
iconSize: 19,
iconEnabledColor:
Theme.of(context).appBarTheme.foregroundColor!,
iconDisabledColor:
Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
const Gap(8),
],
),
body: developerStats.when(
data:
(stats) => SingleChildScrollView(
child:
currentDeveloper.value == null
? Column(
children: [
const Gap(24),
const Icon(Symbols.info, size: 32).padding(bottom: 4),
Text(
'developerHubUnselectedHint',
textAlign: TextAlign.center,
).tr(),
const Gap(24),
const Divider(height: 1),
...(developers.value?.map(
(developer) => ListTile(
leading: ProfilePictureWidget(
file: developer.picture,
),
title: Text(developer.nick),
subtitle: Text('@${developer.name}'),
onTap: () {
currentDeveloper.value = developer;
},
),
) ??
[]),
ListTile(
leading: const CircleAvatar(
child: Icon(Symbols.add),
),
title: Text('enrollDeveloper').tr(),
subtitle: Text('enrollDeveloperHint').tr(),
trailing: const Icon(Symbols.chevron_right),
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder:
(_) => const _DeveloperEnrollmentSheet(),
).then((value) {
if (value == true) {
ref.invalidate(developersProvider);
}
});
},
),
],
)
: Column(
children: [
if (stats != null)
_DeveloperStatsWidget(
stats: stats,
).padding(vertical: 12, horizontal: 12),
ListTile(
minTileHeight: 48,
title: Text('customApps').tr(),
trailing: Icon(Symbols.chevron_right),
leading: const Icon(Symbols.apps),
contentPadding: EdgeInsets.symmetric(
horizontal: 24,
),
onTap: () {
context.push(
'/developers/${currentDeveloper.value!.name}/apps',
);
},
),
],
),
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(err, stack) => ResponseErrorWidget(
error: err,
onRetry: () {
ref.invalidate(
developerStatsProvider(currentDeveloper.value?.name),
);
},
),
),
);
}
}
class _DeveloperStatsWidget extends StatelessWidget {
final DeveloperStats stats;
const _DeveloperStatsWidget({required this.stats});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
spacing: 8,
children: [
Row(
spacing: 8,
children: [
Expanded(
child: _buildStatsCard(
context,
stats.totalCustomApps.toString(),
'totalCustomApps',
),
),
],
),
],
),
);
}
Widget _buildStatsCard(
BuildContext context,
String statValue,
String statLabel,
) {
return Card(
margin: EdgeInsets.zero,
child: SizedBox(
height: 100,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
statValue,
style: Theme.of(context).textTheme.headlineMedium,
),
const Gap(4),
Text(
statLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
).tr(),
],
),
),
),
);
}
}
class _DeveloperEnrollmentSheet extends HookConsumerWidget {
const _DeveloperEnrollmentSheet();
@override
Widget build(BuildContext context, WidgetRef ref) {
final publishers = ref.watch(publishersManagedProvider);
Future<void> enroll(SnPublisher publisher) async {
try {
final client = ref.read(apiClientProvider);
await client.post('/developers/${publisher.name}/enroll');
if (context.mounted) {
Navigator.pop(context, true);
}
} catch (err) {
showErrorAlert(err);
}
}
return SheetScaffold(
titleText: 'enrollDeveloper'.tr(),
child: publishers.when(
data:
(items) =>
items.isEmpty
? Center(
child:
Text(
'noPublishersToEnroll',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final publisher = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: publisher.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(publisher.nick),
subtitle: Text('@${publisher.name}'),
onTap: () => enroll(publisher),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
error: error,
onRetry: () => ref.invalidate(publishersManagedProvider),
),
),
);
}
}

View File

@ -0,0 +1,172 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hub.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$developerStatsHash() => r'783398cbde09c3d956c3e20b02a1cebd1f8ab748';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [developerStats].
@ProviderFor(developerStats)
const developerStatsProvider = DeveloperStatsFamily();
/// See also [developerStats].
class DeveloperStatsFamily extends Family<AsyncValue<DeveloperStats?>> {
/// See also [developerStats].
const DeveloperStatsFamily();
/// See also [developerStats].
DeveloperStatsProvider call(String? uname) {
return DeveloperStatsProvider(uname);
}
@override
DeveloperStatsProvider getProviderOverride(
covariant DeveloperStatsProvider provider,
) {
return call(provider.uname);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'developerStatsProvider';
}
/// See also [developerStats].
class DeveloperStatsProvider
extends AutoDisposeFutureProvider<DeveloperStats?> {
/// See also [developerStats].
DeveloperStatsProvider(String? uname)
: this._internal(
(ref) => developerStats(ref as DeveloperStatsRef, uname),
from: developerStatsProvider,
name: r'developerStatsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$developerStatsHash,
dependencies: DeveloperStatsFamily._dependencies,
allTransitiveDependencies:
DeveloperStatsFamily._allTransitiveDependencies,
uname: uname,
);
DeveloperStatsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.uname,
}) : super.internal();
final String? uname;
@override
Override overrideWith(
FutureOr<DeveloperStats?> Function(DeveloperStatsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DeveloperStatsProvider._internal(
(ref) => create(ref as DeveloperStatsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
uname: uname,
),
);
}
@override
AutoDisposeFutureProviderElement<DeveloperStats?> createElement() {
return _DeveloperStatsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DeveloperStatsProvider && other.uname == uname;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, uname.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin DeveloperStatsRef on AutoDisposeFutureProviderRef<DeveloperStats?> {
/// The parameter `uname` of this provider.
String? get uname;
}
class _DeveloperStatsProviderElement
extends AutoDisposeFutureProviderElement<DeveloperStats?>
with DeveloperStatsRef {
_DeveloperStatsProviderElement(super.provider);
@override
String? get uname => (origin as DeveloperStatsProvider).uname;
}
String _$developersHash() => r'f52639d3c21aafbf235c8ae33f35448baf2989a1';
/// See also [developers].
@ProviderFor(developers)
final developersProvider =
AutoDisposeFutureProvider<List<SnPublisher>>.internal(
developers,
name: r'developersProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$developersHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef DevelopersRef = AutoDisposeFutureProviderRef<List<SnPublisher>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
import 'package:island/screens/developers/edit_app.dart';
class NewCustomAppScreen extends StatelessWidget {
final String publisherName;
const NewCustomAppScreen({super.key, required this.publisherName});
@override
Widget build(BuildContext context) {
return EditAppScreen(publisherName: publisherName);
}
}

View File

@ -0,0 +1,142 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/web_article_card.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
part 'articles.g.dart';
@riverpod
class ArticlesListNotifier extends _$ArticlesListNotifier
with CursorPagingNotifierMixin<SnWebArticle> {
static const int _pageSize = 20;
Map<String, dynamic> _params = {};
@override
Future<CursorPagingData<SnWebArticle>> build({
String? feedId,
String? publisherId,
}) async {
_params = {
if (feedId != null) 'feedId': feedId,
if (publisherId != null) 'publisherId': publisherId,
};
return fetch(cursor: null);
}
@override
Future<CursorPagingData<SnWebArticle>> fetch({
required String? cursor,
}) async {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final queryParams = {'limit': _pageSize, 'offset': offset, ..._params};
try {
final response = await client.get(
'/feeds/articles',
queryParameters: queryParams,
);
final List<dynamic> data = response.data;
final articles =
data
.map(
(json) => SnWebArticle.fromJson(json as Map<String, dynamic>),
)
.toList();
final total = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0;
final hasMore = offset + articles.length < total;
final nextCursor = hasMore ? (offset + articles.length).toString() : null;
return CursorPagingData(
items: articles,
hasMore: hasMore,
nextCursor: nextCursor,
);
} catch (e) {
debugPrint('Error fetching articles: $e');
rethrow;
}
}
}
class SliverArticlesList extends ConsumerWidget {
final String? feedId;
final String? publisherId;
final Color? backgroundColor;
final EdgeInsets? padding;
final Function? onRefresh;
const SliverArticlesList({
super.key,
this.feedId,
this.publisherId,
this.backgroundColor,
this.padding,
this.onRefresh,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PagingHelperSliverView(
provider: articlesListNotifierProvider(
feedId: feedId,
publisherId: publisherId,
),
futureRefreshable:
articlesListNotifierProvider(
feedId: feedId,
publisherId: publisherId,
).future,
notifierRefreshable:
articlesListNotifierProvider(
feedId: feedId,
publisherId: publisherId,
).notifier,
contentBuilder:
(data, widgetCount, endItemView) => SliverList.builder(
itemCount: widgetCount,
itemBuilder: (context, index) {
if (index == widgetCount - 1) {
return endItemView;
}
final article = data.items[index];
return WebArticleCard(article: article, showDetails: true);
},
),
);
}
}
class ArticlesScreen extends ConsumerWidget {
final String? feedId;
final String? publisherId;
final String? title;
const ArticlesScreen({super.key, this.feedId, this.publisherId, this.title});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: Text(title ?? 'Articles')),
body: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
sliver: SliverArticlesList(
feedId: feedId,
publisherId: publisherId,
),
),
],
),
);
}
}

View File

@ -0,0 +1,206 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'articles.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$articlesListNotifierHash() =>
r'924f2344c3bbf0ff7b92fe69e88d3b64a534b538';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$ArticlesListNotifier
extends BuildlessAutoDisposeAsyncNotifier<CursorPagingData<SnWebArticle>> {
late final String? feedId;
late final String? publisherId;
FutureOr<CursorPagingData<SnWebArticle>> build({
String? feedId,
String? publisherId,
});
}
/// See also [ArticlesListNotifier].
@ProviderFor(ArticlesListNotifier)
const articlesListNotifierProvider = ArticlesListNotifierFamily();
/// See also [ArticlesListNotifier].
class ArticlesListNotifierFamily
extends Family<AsyncValue<CursorPagingData<SnWebArticle>>> {
/// See also [ArticlesListNotifier].
const ArticlesListNotifierFamily();
/// See also [ArticlesListNotifier].
ArticlesListNotifierProvider call({String? feedId, String? publisherId}) {
return ArticlesListNotifierProvider(
feedId: feedId,
publisherId: publisherId,
);
}
@override
ArticlesListNotifierProvider getProviderOverride(
covariant ArticlesListNotifierProvider provider,
) {
return call(feedId: provider.feedId, publisherId: provider.publisherId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'articlesListNotifierProvider';
}
/// See also [ArticlesListNotifier].
class ArticlesListNotifierProvider
extends
AutoDisposeAsyncNotifierProviderImpl<
ArticlesListNotifier,
CursorPagingData<SnWebArticle>
> {
/// See also [ArticlesListNotifier].
ArticlesListNotifierProvider({String? feedId, String? publisherId})
: this._internal(
() =>
ArticlesListNotifier()
..feedId = feedId
..publisherId = publisherId,
from: articlesListNotifierProvider,
name: r'articlesListNotifierProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$articlesListNotifierHash,
dependencies: ArticlesListNotifierFamily._dependencies,
allTransitiveDependencies:
ArticlesListNotifierFamily._allTransitiveDependencies,
feedId: feedId,
publisherId: publisherId,
);
ArticlesListNotifierProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.feedId,
required this.publisherId,
}) : super.internal();
final String? feedId;
final String? publisherId;
@override
FutureOr<CursorPagingData<SnWebArticle>> runNotifierBuild(
covariant ArticlesListNotifier notifier,
) {
return notifier.build(feedId: feedId, publisherId: publisherId);
}
@override
Override overrideWith(ArticlesListNotifier Function() create) {
return ProviderOverride(
origin: this,
override: ArticlesListNotifierProvider._internal(
() =>
create()
..feedId = feedId
..publisherId = publisherId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
feedId: feedId,
publisherId: publisherId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<
ArticlesListNotifier,
CursorPagingData<SnWebArticle>
>
createElement() {
return _ArticlesListNotifierProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ArticlesListNotifierProvider &&
other.feedId == feedId &&
other.publisherId == publisherId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, feedId.hashCode);
hash = _SystemHash.combine(hash, publisherId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ArticlesListNotifierRef
on AutoDisposeAsyncNotifierProviderRef<CursorPagingData<SnWebArticle>> {
/// The parameter `feedId` of this provider.
String? get feedId;
/// The parameter `publisherId` of this provider.
String? get publisherId;
}
class _ArticlesListNotifierProviderElement
extends
AutoDisposeAsyncNotifierProviderElement<
ArticlesListNotifier,
CursorPagingData<SnWebArticle>
>
with ArticlesListNotifierRef {
_ArticlesListNotifierProviderElement(super.provider);
@override
String? get feedId => (origin as ArticlesListNotifierProvider).feedId;
@override
String? get publisherId =>
(origin as ArticlesListNotifierProvider).publisherId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -6,7 +6,9 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/activity.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/realm.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
@ -20,6 +22,7 @@ import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/realm/realm_card.dart';
import 'package:island/widgets/publisher/publisher_card.dart';
import 'package:island/widgets/web_article_card.dart';
import 'package:styled_widget/styled_widget.dart';
part 'explore.g.dart';
@ -90,37 +93,83 @@ class ExploreScreen extends HookConsumerWidget {
extendBody: false, // Prevent conflicts with tabs navigation
appBar: AppBar(
toolbarHeight: 0,
bottom: TabBar(
controller: tabController,
tabs: [
Tab(
child: Text(
'explore'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Row(
children: [
Expanded(
child: TabBar(
controller: tabController,
tabAlignment: TabAlignment.start,
isScrollable: true,
dividerColor: Colors.transparent,
tabs: [
Tab(
icon: Tooltip(
message: 'explore'.tr(),
child: Icon(
Symbols.explore,
color:
Theme.of(
context,
).appBarTheme.foregroundColor!,
),
),
),
Tab(
icon: Tooltip(
message: 'exploreFilterSubscriptions'.tr(),
child: Icon(
Symbols.subscriptions,
color:
Theme.of(
context,
).appBarTheme.foregroundColor!,
),
),
),
Tab(
icon: Tooltip(
message: 'exploreFilterFriends'.tr(),
child: Icon(
Symbols.people,
color:
Theme.of(
context,
).appBarTheme.foregroundColor!,
),
),
),
],
),
),
IconButton(
onPressed: () {
context.push('/feeds/articles');
},
icon: Icon(
Symbols.auto_stories,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'webArticlesStand'.tr(),
),
IconButton(
onPressed: () {
context.push('/posts/search');
},
icon: Icon(
Symbols.search,
color: Theme.of(context).appBarTheme.foregroundColor!,
),
tooltip: 'search'.tr(),
),
],
)
.padding(horizontal: 8)
.border(
bottom: 1 / MediaQuery.of(context).devicePixelRatio,
color: Theme.of(context).dividerColor,
),
),
Tab(
child: Text(
'exploreFilterSubscriptions'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
Tab(
child: Text(
'exploreFilterFriends'.tr(),
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor!,
),
),
),
],
),
),
floatingActionButton: FloatingActionButton(
@ -195,6 +244,7 @@ class _DiscoveryActivityItem extends StatelessWidget {
(switch (type) {
'realm' => 'discoverRealms',
'publisher' => 'discoverPublishers',
'article' => 'discoverWebArticles',
_ => 'unknown',
}).tr(),
style: Theme.of(context).textTheme.titleMedium,
@ -220,6 +270,11 @@ class _DiscoveryActivityItem extends StatelessWidget {
publisher: SnPublisher.fromJson(item['data']),
maxWidth: 280,
);
case 'article':
return WebArticleCard(
article: SnWebArticle.fromJson(item['data']),
maxWidth: 280,
);
default:
return Placeholder();
}
@ -341,7 +396,7 @@ class ActivityListNotifier extends _$ActivityListNotifier
if (cursor != null) 'cursor': cursor,
'take': take,
if (filter != null) 'filter': filter,
if (kDebugMode) 'debugInclude': 'realms,publishers',
if (kDebugMode) 'debugInclude': 'realms,publishers,articles',
};
final response = await client.get(

View File

@ -7,7 +7,7 @@ part of 'explore.dart';
// **************************************************************************
String _$activityListNotifierHash() =>
r'57e9dcec944a9f88f8508b69fc91342592f5b349';
r'98b62fb9b958023d2c9e320af7ec1f1244836f49';
/// Copied from Dart SDK
class _SystemHash {

View File

@ -4,19 +4,18 @@ import 'dart:math' as math;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/route.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/content/markdown.dart';
import 'package:relative_time/relative_time.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:url_launcher/url_launcher_string.dart';
part 'notification.g.dart';
@ -180,36 +179,17 @@ class NotificationScreen extends HookConsumerWidget {
),
),
onTap: () {
if (notification.meta['link'] is String) {
final href = notification.meta['link'];
final uri = Uri.tryParse(href);
if (uri == null) {
showSnackBar(
'brokenLink'.tr(args: []),
action: SnackBarAction(
label: 'copyToClipboard'.tr(),
onPressed: () {
Clipboard.setData(ClipboardData(text: href));
clearSnackBar(context);
},
),
if (notification.meta['action_uri'] != null) {
var uri = notification.meta['action_uri'] as String;
if (uri.startsWith('/')) {
// In-app routes
rootNavigatorKey.currentContext?.push(
notification.meta['action_uri'],
);
return;
} else {
// External URLs
launchUrlString(uri);
}
if (uri.scheme == 'solian') {
context.push(
['', uri.host, ...uri.pathSegments].join('/'),
);
return;
}
showConfirmAlert(
'openLinkConfirmDescription'.tr(args: [href]),
'openLinkConfirm'.tr(),
).then((value) {
if (value) {
launchUrl(uri, mode: LaunchMode.externalApplication);
}
});
}
},
);

View File

@ -15,7 +15,7 @@ import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/post/compose_shared.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/publishers_modal.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/widgets/post/compose_settings_sheet.dart';
import 'package:island/services/compose_storage_db.dart';
import 'package:island/widgets/post/draft_manager.dart';

View File

@ -12,7 +12,7 @@ import 'package:island/models/post.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/services/responsive.dart';
import 'package:island/widgets/app_scaffold.dart';
import 'package:island/screens/posts/detail.dart';
import 'package:island/screens/posts/post_detail.dart';
import 'package:island/widgets/content/attachment_preview.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/markdown.dart';

View File

@ -9,10 +9,11 @@ import 'package:island/widgets/app_scaffold.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:island/widgets/post/post_quick_reply.dart';
import 'package:island/widgets/post/post_replies.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
part 'detail.g.dart';
part 'post_detail.g.dart';
@riverpod
Future<SnPost?> post(Ref ref, String id) async {
@ -21,20 +22,43 @@ Future<SnPost?> post(Ref ref, String id) async {
return SnPost.fromJson(resp.data);
}
final postStateProvider = StateNotifierProvider.family<PostState, AsyncValue<SnPost?>, String>(
(ref, id) => PostState(ref, id),
);
class PostState extends StateNotifier<AsyncValue<SnPost?>> {
final Ref _ref;
final String _id;
PostState(this._ref, this._id) : super(const AsyncValue.loading()) {
// Initialize with the initial post data
_ref.listen<AsyncValue<SnPost?>>(
postProvider(_id),
(_, next) => state = next,
);
}
void updatePost(SnPost? newPost) {
if (newPost != null) {
state = AsyncData(newPost);
}
}
}
class PostDetailScreen extends HookConsumerWidget {
final String id;
const PostDetailScreen({super.key, required this.id});
@override
Widget build(BuildContext context, WidgetRef ref) {
final post = ref.watch(postProvider(id));
final postState = ref.watch(postStateProvider(id));
final user = ref.watch(userInfoProvider);
final isWide = isWideScreen(context);
return AppScaffold(
appBar: AppBar(title: const Text('Post')),
body: post.when(
body: postState.when(
data: (post) {
return Stack(
fit: StackFit.expand,
@ -49,6 +73,10 @@ class PostDetailScreen extends HookConsumerWidget {
isOpenable: false,
isFullPost: true,
backgroundColor: isWide ? Colors.transparent : null,
onUpdate: (newItem) {
// Update the local state with the new post data
ref.read(postStateProvider(id).notifier).updatePost(newItem);
},
),
const Divider(height: 1),
],
@ -65,11 +93,15 @@ class PostDetailScreen extends HookConsumerWidget {
right: 0,
child: Material(
elevation: 2,
child: PostQuickReply(
parent: post,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(id));
},
child: postState.when(
data: (post) => PostQuickReply(
parent: post!,
onPosted: () {
ref.invalidate(postRepliesNotifierProvider(id));
},
),
loading: () => const SizedBox.shrink(),
error: (_, __) => const SizedBox.shrink(),
).padding(
bottom: MediaQuery.of(context).padding.bottom + 16,
top: 16,

View File

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'detail.dart';
part of 'post_detail.dart';
// **************************************************************************
// RiverpodGenerator

View File

@ -0,0 +1,165 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/post/post_item.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
final postSearchNotifierProvider = StateNotifierProvider.autoDispose<
PostSearchNotifier,
AsyncValue<CursorPagingData<SnPost>>
>((ref) => PostSearchNotifier(ref));
class PostSearchNotifier
extends StateNotifier<AsyncValue<CursorPagingData<SnPost>>> {
final AutoDisposeRef ref;
static const int _pageSize = 20;
String _currentQuery = '';
bool _isLoading = false;
PostSearchNotifier(this.ref) : super(const AsyncValue.loading()) {
state = const AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
}
Future<void> search(String query) async {
if (_isLoading) return;
_currentQuery = query.trim();
if (_currentQuery.isEmpty) {
state = AsyncValue.data(
CursorPagingData(items: [], hasMore: false, nextCursor: null),
);
return;
}
await fetch(cursor: null);
}
Future<void> fetch({String? cursor}) async {
if (_isLoading) return;
_isLoading = true;
state = const AsyncValue.loading();
try {
final client = ref.read(apiClientProvider);
final offset = cursor == null ? 0 : int.parse(cursor);
final response = await client.get(
'/posts/search',
queryParameters: {
'query': _currentQuery,
'offset': offset,
'take': _pageSize,
'useVector': true,
},
);
final data = response.data as List;
final posts = data.map((json) => SnPost.fromJson(json)).toList();
final hasMore = posts.length == _pageSize;
final nextCursor = hasMore ? (offset + posts.length).toString() : null;
state = AsyncValue.data(
CursorPagingData(
items: posts,
hasMore: hasMore,
nextCursor: nextCursor,
),
);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
} finally {
_isLoading = false;
}
}
}
class PostSearchScreen extends ConsumerStatefulWidget {
const PostSearchScreen({super.key});
@override
ConsumerState<PostSearchScreen> createState() => _PostSearchScreenState();
}
class _PostSearchScreenState extends ConsumerState<PostSearchScreen> {
final _searchController = TextEditingController();
final _debounce = Duration(milliseconds: 500);
Timer? _debounceTimer;
@override
void dispose() {
_searchController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
void _onSearchChanged(String query) {
if (_debounceTimer?.isActive ?? false) _debounceTimer!.cancel();
_debounceTimer = Timer(_debounce, () {
ref.read(postSearchNotifierProvider.notifier).search(query);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: 'Search posts...',
border: InputBorder.none,
hintStyle: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
),
style: TextStyle(
color: Theme.of(context).appBarTheme.foregroundColor,
),
onChanged: _onSearchChanged,
onSubmitted: (value) {
ref.read(postSearchNotifierProvider.notifier).search(value);
},
autofocus: true,
),
),
body: Consumer(
builder: (context, ref, child) {
final searchState = ref.watch(postSearchNotifierProvider);
return searchState.when(
data: (data) {
if (data.items.isEmpty && _searchController.text.isNotEmpty) {
return const Center(child: Text('No results found'));
}
return ListView.builder(
itemCount: data.items.length + (data.hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= data.items.length) {
ref
.read(postSearchNotifierProvider.notifier)
.fetch(cursor: data.nextCursor);
return const Center(child: CircularProgressIndicator());
}
final post = data.items[index];
return Column(
children: [PostItem(item: post), const Divider(height: 1)],
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
);
},
),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/models/user.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';

View File

@ -400,7 +400,7 @@ class _PublisherSubscriptionStatusProviderElement
}
String _$publisherAppbarForcegroundColorHash() =>
r'3ff2eebb48d3f3af1907052f471e648f5b14b13c';
r'd781a806a242aea5c1609ec98c97c52fdd9f7db1';
/// See also [publisherAppbarForcegroundColor].
@ProviderFor(publisherAppbarForcegroundColor)

View File

@ -22,7 +22,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:riverpod_paging_utils/riverpod_paging_utils.dart';
import 'package:styled_widget/styled_widget.dart';
part 'detail.g.dart';
part 'realm_detail.g.dart';
@riverpod
Future<Color?> realmAppbarForegroundColor(Ref ref, String realmSlug) async {

View File

@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'detail.dart';
part of 'realm_detail.dart';
// **************************************************************************
// RiverpodGenerator

View File

@ -294,9 +294,9 @@ class EditRealmScreen extends HookConsumerWidget {
child:
background.value != null
? CloudFileWidget(
item: background.value!,
fit: BoxFit.cover,
)
item: background.value!,
fit: BoxFit.cover,
)
: const SizedBox.shrink(),
),
onTap: () {
@ -344,24 +344,46 @@ class EditRealmScreen extends HookConsumerWidget {
const SizedBox(height: 16),
TextFormField(
controller: descriptionController,
decoration: InputDecoration(labelText: 'description'.tr()),
decoration: InputDecoration(
labelText: 'description'.tr(),
alignLabelWithHint: true,
),
minLines: 3,
maxLines: null,
onTapOutside:
(_) => FocusManager.instance.primaryFocus?.unfocus(),
),
const SizedBox(height: 16),
CheckboxListTile(
title: const Text('isPublic').tr(),
subtitle: const Text('isPublicHint').tr(),
value: isPublic.value,
onChanged: (value) => isPublic.value = value ?? false,
),
CheckboxListTile(
title: const Text('isCommunity').tr(),
subtitle: const Text('isCommunityHint').tr(),
value: isCommunity.value,
onChanged: (value) => isCommunity.value = value ?? false,
Card(
margin: EdgeInsets.zero,
child: Column(
children: [
CheckboxListTile(
secondary: const Icon(Symbols.public),
title: Text('publicRealm').tr(),
subtitle: Text('publicRealmDescription').tr(),
value: isPublic.value,
onChanged: (value) {
isPublic.value = value ?? true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
CheckboxListTile(
secondary: const Icon(Symbols.travel_explore),
title: Text('communityRealm').tr(),
subtitle: Text('communityRealmDescription').tr(),
value: isCommunity.value,
onChanged: (value) {
isCommunity.value = value ?? false;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
],
),
),
const SizedBox(height: 16),
Align(
@ -435,47 +457,47 @@ class _RealmInviteSheet extends HookConsumerWidget {
(items) =>
items.isEmpty
? Center(
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
child:
Text(
'invitesEmpty',
textAlign: TextAlign.center,
).tr(),
)
: ListView.builder(
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
shrinkWrap: true,
itemCount: items.length,
itemBuilder: (context, index) {
final invite = items[index];
return ListTile(
leading: ProfilePictureWidget(
fileId: invite.realm!.picture?.id,
fallbackIcon: Symbols.group,
),
title: Text(invite.realm!.name),
subtitle:
Text(
invite.role >= 100
? 'permissionOwner'
: invite.role >= 50
? 'permissionModerator'
: 'permissionMember',
).tr(),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Symbols.check),
onPressed: () => acceptInvite(invite),
),
IconButton(
icon: const Icon(Symbols.close),
onPressed: () => declineInvite(invite),
),
],
),
);
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error:
(error, _) => ResponseErrorWidget(
@ -485,4 +507,4 @@ class _RealmInviteSheet extends HookConsumerWidget {
),
);
}
}
}

View File

@ -341,6 +341,25 @@ class SettingsScreen extends HookConsumerWidget {
];
final behaviorSettings = [
ListTile(
minLeadingWidth: 48,
title: Text('creatorHub').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.rocket_launch),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.push('/creators'),
),
// Developer Hub
ListTile(
minLeadingWidth: 48,
title: Text('developerHub').tr(),
contentPadding: const EdgeInsets.only(left: 24, right: 17),
leading: const Icon(Symbols.hub),
trailing: const Icon(Symbols.chevron_right),
onTap: () => context.push('/developers'),
),
// Auto translate settings
ListTile(
minLeadingWidth: 48,

View File

@ -0,0 +1 @@

View File

@ -44,7 +44,7 @@ class AudioCallButton extends HookConsumerWidget {
try {
await apiClient.post('/chat/realtime/$roomId');
if (context.mounted) {
context.push('/chat/call/roomId');
context.push('/chat/call/$roomId');
}
} catch (e) {
showErrorAlert(e);
@ -96,7 +96,7 @@ class AudioCallButton extends HookConsumerWidget {
tooltip: 'Join Ongoing Call',
onPressed: () {
if (context.mounted) {
context.push('/chat/call/roomId');
context.push('/chat/$roomId/call');
}
},
);

View File

@ -360,7 +360,7 @@ class CallOverlayBar extends HookConsumerWidget {
).padding(all: 16),
),
onTap: () {
context.push('/chat/call/callNotifier.roomId!');
context.push('/chat/call/${callNotifier.roomId!}');
},
);
}

View File

@ -13,6 +13,7 @@ import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/content/cloud_files.dart';
import 'package:island/widgets/content/sheet.dart';
import 'package:path/path.dart' show extension;
import 'package:path_provider/path_provider.dart';
import 'package:photo_view/photo_view.dart';
@ -125,6 +126,7 @@ class CloudFileList extends HookConsumerWidget {
if (!disableZoomIn) {
context.pushTransparentRoute(
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
rootNavigator: true,
);
}
},
@ -209,6 +211,124 @@ class CloudFileZoomIn extends HookConsumerWidget {
}
}
Widget buildInfoRow(IconData icon, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
child: Row(
children: [
Icon(
icon,
size: 20,
color: Theme.of(context).colorScheme.onSurface,
),
const SizedBox(width: 12),
Text(
label,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color,
),
),
const Spacer(),
Flexible(
child: Text(
value,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
textAlign: TextAlign.end,
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
String _formatFileSize(int bytes) {
if (bytes <= 0) return '0 B';
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) return '${(bytes / 1024).toStringAsFixed(2)} KB';
if (bytes < 1024 * 1024 * 1024) {
return '${(bytes / (1024 * 1024)).toStringAsFixed(2)} MB';
}
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
}
void showInfoSheet() {
final theme = Theme.of(context);
final exifData = item.fileMeta?['exif'] as Map<String, dynamic>? ?? {};
showModalBottomSheet(
useRootNavigator: true,
context: context,
isScrollControlled: true,
builder:
(context) => SheetScaffold(
titleText: 'File Information',
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buildInfoRow(Icons.description, 'Name', item.name),
const Divider(height: 1),
buildInfoRow(
Icons.storage,
'Size',
_formatFileSize(item.size),
),
const Divider(height: 1),
buildInfoRow(
Icons.category,
'Type',
item.mimeType?.toUpperCase() ?? 'UNKNOWN',
),
if (exifData.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
'EXIF Data',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
).padding(horizontal: 24),
const SizedBox(height: 8),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...exifData.entries.map(
(entry) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${entry.key.contains('-') ? entry.key.split('-').last : entry.key}: ',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
Expanded(
child: Text(
'${entry.value}'.isNotEmpty
? '${entry.value}'
: 'N/A',
style: theme.textTheme.bodyMedium,
),
),
],
),
),
),
],
).padding(horizontal: 24),
],
const SizedBox(height: 16),
],
),
),
),
);
}
return DismissiblePage(
isFullScreen: true,
backgroundColor: Colors.transparent,
@ -287,8 +407,22 @@ class CloudFileZoomIn extends HookConsumerWidget {
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: Icon(
Icons.info_outline,
color: Colors.white,
shadows: [
Shadow(
color: Colors.black54,
blurRadius: 5.0,
offset: Offset(1.0, 1.0),
),
],
),
onPressed: showInfoSheet,
),
Spacer(),
IconButton(
icon: Icon(Icons.remove, color: Colors.white),
onPressed: () {

View File

@ -74,9 +74,7 @@ class MarkdownTextContent extends HookConsumerWidget {
final url = Uri.tryParse(href);
if (url != null) {
if (url.scheme == 'solian') {
context.push(
['', url.host, ...url.pathSegments].join('/'),
);
context.push(['', url.host, ...url.pathSegments].join('/'));
return;
}
final whitelistDomains = ['solian.app', 'solsynth.dev'];
@ -143,17 +141,27 @@ class MarkdownTextContent extends HookConsumerWidget {
),
],
),
generator: MarkdownGenerator(
generators: [latexGenerator],
inlineSyntaxList: [
_UserNameCardInlineSyntax(),
_StickerInlineSyntax(),
LatexSyntax(isDark),
],
linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4),
generator: MarkdownTextContent.buildGenerator(
isDark: isDark,
linesMargin: linesMargin,
),
);
}
static MarkdownGenerator buildGenerator({
bool isDark = false,
EdgeInsets? linesMargin,
}) {
return MarkdownGenerator(
generators: [latexGenerator],
inlineSyntaxList: [
_UserNameCardInlineSyntax(),
_StickerInlineSyntax(),
LatexSyntax(isDark),
],
linesMargin: linesMargin ?? EdgeInsets.symmetric(vertical: 4),
);
}
}
class _UserNameCardInlineSyntax extends markdown.InlineSyntax {

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
class EmptyState extends StatelessWidget {
final IconData icon;
final String title;
final String description;
final Widget? action;
const EmptyState({
super.key,
required this.icon,
required this.title,
required this.description,
this.action,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
icon,
size: 64,
color: Theme.of(context).colorScheme.outline,
),
const SizedBox(height: 16),
Text(
title,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
description,
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
),
if (action != null) ...[
const SizedBox(height: 24),
action!,
],
],
),
),
);
}
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
/// A simple loading indicator widget that can be used throughout the app
class LoadingIndicator extends StatelessWidget {
/// The size of the loading indicator
final double size;
/// The color of the loading indicator
final Color? color;
/// Creates a loading indicator
const LoadingIndicator({
super.key,
this.size = 24.0,
this.color,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: color != null
? AlwaysStoppedAnimation<Color>(
color!,
)
: null,
),
);
}
}

View File

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:image_picker/image_picker.dart';
import 'package:island/models/file.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/services/file.dart';

View File

@ -362,6 +362,7 @@ class PostItem extends HookConsumerWidget {
showModalBottomSheet(
context: context,
isScrollControlled: true,
useRootNavigator: true,
builder: (context) => PostRepliesSheet(post: item),
);
}
@ -535,20 +536,20 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
),
],
),
).gestures(onTap: () => context.push('/posts/referencePost.id'));
).gestures(onTap: () => context.push('/posts/${referencePost.id}'));
}
class PostReactionList extends HookConsumerWidget {
final String parentId;
final Map<String, int> reactions;
final Function(String symbol, int attitude, int delta) onReact;
final Function(String symbol, int attitude, int delta)? onReact;
final EdgeInsets? padding;
const PostReactionList({
super.key,
required this.parentId,
required this.reactions,
this.padding,
required this.onReact,
this.onReact,
});
@override
@ -569,7 +570,7 @@ class PostReactionList extends HookConsumerWidget {
})
.then((resp) {
var isRemoving = resp.statusCode == 204;
onReact(symbol, attitude, isRemoving ? -1 : 1);
onReact?.call(symbol, attitude, isRemoving ? -1 : 1);
HapticFeedback.heavyImpact();
});
submitting.value = false;
@ -581,33 +582,34 @@ class PostReactionList extends HookConsumerWidget {
scrollDirection: Axis.horizontal,
padding: padding ?? EdgeInsets.zero,
children: [
Padding(
padding: const EdgeInsets.only(right: 8),
child: ActionChip(
avatar: Icon(Symbols.add_reaction),
label: Text('react').tr(),
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
if (onReact != null)
Padding(
padding: const EdgeInsets.only(right: 8),
child: ActionChip(
avatar: Icon(Symbols.add_reaction),
label: Text('react').tr(),
visualDensity: const VisualDensity(
horizontal: VisualDensity.minimumDensity,
vertical: VisualDensity.minimumDensity,
),
onPressed:
submitting.value
? null
: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return _PostReactionSheet(
reactionsCount: reactions,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
},
);
},
);
},
),
onPressed:
submitting.value
? null
: () {
showModalBottomSheet(
context: context,
builder: (BuildContext context) {
return _PostReactionSheet(
reactionsCount: reactions,
onReact: (symbol, attitude) {
reactPost(symbol, attitude);
},
);
},
);
},
),
),
for (final symbol in reactions.keys)
Padding(
padding: const EdgeInsets.only(right: 8),

View File

@ -365,11 +365,6 @@ class PostItemCreator extends HookConsumerWidget {
parentId: item.id,
reactions: item.reactionsCount,
padding: EdgeInsets.zero,
onReact: (symbol, attitude, delta) {
final reactionsCount = Map<String, int>.from(item.reactionsCount);
reactionsCount[symbol] = (reactionsCount[symbol] ?? 0) + delta;
onUpdate?.call(item.copyWith(reactionsCount: reactionsCount));
},
),
const Gap(16),
],

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/pods/network.dart';
import 'package:island/screens/creators/publishers.dart';
import 'package:island/widgets/alert.dart';

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:island/models/post.dart';
import 'package:island/models/publisher.dart';
import 'package:island/widgets/content/cloud_files.dart';
class PublisherCard extends ConsumerWidget {

View File

@ -11,7 +11,6 @@ import 'package:island/models/file.dart';
import 'package:island/pods/link_preview.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/services/file.dart';
import 'package:mime/mime.dart';
@ -193,7 +192,6 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
setState(() => _isLoading = true);
try {
final apiClient = ref.read(apiClientProvider);
final userInfo = ref.read(userInfoProvider.notifier);
final serverUrl = ref.read(serverUrlProvider);
String content = _messageController.text.trim();
@ -218,7 +216,7 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
case ShareContentType.file:
// Upload files to cloud storage
if (widget.content.files?.isNotEmpty == true) {
final token = await userInfo.getAccessToken();
final token = ref.watch(tokenProvider)?.token;
if (token == null) {
throw Exception('Authentication required');
}

View File

@ -0,0 +1,139 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:island/models/webfeed.dart';
import 'package:island/services/time.dart';
class WebArticleCard extends StatelessWidget {
final SnWebArticle article;
final double? maxWidth;
final bool showDetails;
const WebArticleCard({
super.key,
required this.article,
this.maxWidth,
this.showDetails = false,
});
void _onTap(BuildContext context) {
context.push('/feeds/articles/${article.id}');
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
child: Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () => _onTap(context),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
fit: StackFit.expand,
children: [
// Image or fallback
article.preview?.imageUrl != null
? CachedNetworkImage(
imageUrl: article.preview!.imageUrl!,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
)
: ColoredBox(
color: colorScheme.secondaryContainer,
child: const Center(
child: Icon(
Icons.article_outlined,
size: 48,
color: Colors.white,
),
),
),
// Gradient overlay
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withOpacity(0.7),
],
),
),
),
// Title
Align(
alignment: Alignment.bottomLeft,
child: Container(
padding: const EdgeInsets.only(
left: 12,
right: 12,
bottom: 8,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [
if (showDetails)
const SizedBox(height: 8)
else
Spacer(),
Text(
article.title,
style: theme.textTheme.titleSmall?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
height: 1.3,
),
maxLines: showDetails ? 3 : 2,
overflow: TextOverflow.ellipsis,
),
if (showDetails &&
article.author?.isNotEmpty == true) ...[
const SizedBox(height: 4),
Text(
article.author!,
style: TextStyle(
fontSize: 10,
color: Colors.white.withOpacity(0.9),
fontWeight: FontWeight.w500,
),
),
],
if (showDetails) const Spacer(),
if (showDetails && article.publishedAt != null) ...[
Text(
'${article.publishedAt!.formatSystem()} · ${article.publishedAt!.formatRelative(context)}',
style: const TextStyle(
fontSize: 9,
color: Colors.white70,
),
),
const SizedBox(height: 2),
],
Text(
article.feed?.title ?? 'Unknown Source',
style: const TextStyle(
fontSize: 9,
color: Colors.white70,
),
),
],
),
),
),
],
),
),
),
),
);
}
}

View File

@ -11,32 +11,32 @@ PODS:
- FlutterMacOS
- file_selector_macos (0.0.1):
- FlutterMacOS
- Firebase/CoreOnly (11.13.0):
- FirebaseCore (~> 11.13.0)
- Firebase/Messaging (11.13.0):
- Firebase/CoreOnly (11.15.0):
- FirebaseCore (~> 11.15.0)
- Firebase/Messaging (11.15.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 11.13.0)
- firebase_core (3.14.0):
- Firebase/CoreOnly (~> 11.13.0)
- FirebaseMessaging (~> 11.15.0)
- firebase_core (3.15.0):
- Firebase/CoreOnly (~> 11.15.0)
- FlutterMacOS
- firebase_messaging (15.2.7):
- Firebase/CoreOnly (~> 11.13.0)
- Firebase/Messaging (~> 11.13.0)
- firebase_messaging (15.2.8):
- Firebase/CoreOnly (~> 11.15.0)
- Firebase/Messaging (~> 11.15.0)
- firebase_core
- FlutterMacOS
- FirebaseCore (11.13.0):
- FirebaseCoreInternal (~> 11.13.0)
- FirebaseCore (11.15.0):
- FirebaseCoreInternal (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (11.13.0):
- FirebaseCoreInternal (11.15.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseInstallations (11.15.0):
- FirebaseCore (~> 11.15.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (11.13.0):
- FirebaseCore (~> 11.13.0)
- FirebaseMessaging (11.15.0):
- FirebaseCore (~> 11.15.0)
- FirebaseInstallations (~> 11.0)
- GoogleDataTransport (~> 10.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
@ -92,7 +92,7 @@ PODS:
- GoogleUtilities/Privacy
- irondash_engine_context (0.0.1):
- FlutterMacOS
- livekit_client (2.4.8):
- livekit_client (2.4.9):
- flutter_webrtc
- FlutterMacOS
- WebRTC-SDK (= 125.6422.07)
@ -291,13 +291,13 @@ SPEC CHECKSUMS:
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
file_selector_macos: 6280b52b459ae6c590af5d78fc35c7267a3c4b31
Firebase: 3435bc66b4d494c2f22c79fd3aae4c1db6662327
firebase_core: 1095fcf33161d99bc34aa10f7c0d89414a208d15
firebase_messaging: 6417056ffb85141607618ddfef9fec9f3caab3ea
FirebaseCore: c692c7f1c75305ab6aff2b367f25e11d73aa8bd0
FirebaseCoreInternal: 29d7b3af4aaf0b8f3ed20b568c13df399b06f68c
FirebaseInstallations: 0ee9074f2c1e86561ace168ee1470dc67aabaf02
FirebaseMessaging: 195bbdb73e6ca1dbc76cd46e73f3552c084ef6e4
Firebase: d99ac19b909cd2c548339c2241ecd0d1599ab02e
firebase_core: 177f51be1650b15d2d5b9f1abf48792619288070
firebase_messaging: 8748a5d4bb435993cffa7f5501292f3e914a23d7
FirebaseCore: efb3893e5b94f32b86e331e3bd6dadf18b66568e
FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4
FirebaseInstallations: 317270fec08a5d418fdbc8429282238cab3ac843
FirebaseMessaging: 3b26e2cee503815e01c3701236b020aa9b576f09
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_platform_alert: 8fa7a7c21f95b26d08b4a3891936ca27e375f284
flutter_secure_storage_macos: 7f45e30f838cf2659862a4e4e3ee1c347c2b3b54
@ -309,7 +309,7 @@ SPEC CHECKSUMS:
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
irondash_engine_context: 893c7d96d20ce361d7e996f39d360c4c2f9869ba
livekit_client: 6a35243df3da61750c98e266e02dedcf5d25c888
livekit_client: c9d9f41996f5cf22b9ba0e8483e6af4ca5094059
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758

View File

@ -13,10 +13,10 @@ packages:
dependency: transitive
description:
name: _flutterfire_internals
sha256: dda4fd7909a732a014239009aa52537b136f8ce568de23c212587097887e2307
sha256: "50e24b769bd1e725732f0aff18b806b8731c1fbcf4e8018ab98e7c4805a2a52f"
url: "https://pub.dev"
source: hosted
version: "1.3.56"
version: "1.3.57"
analyzer:
dependency: transitive
description:
@ -629,50 +629,50 @@ packages:
dependency: "direct main"
description:
name: firebase_core
sha256: "420d9111dcf095341f1ea8fdce926eef750cf7b9745d21f38000de780c94f608"
sha256: "5bba5924139e91d26446fd2601c18a6aa62c1161c768a989bb5e245dcdc20644"
url: "https://pub.dev"
source: hosted
version: "3.14.0"
version: "3.15.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
sha256: "5d2ab45779d91af2aa0252dec9fe4ee1caa015d83377de255454dcaa1526a0e0"
url: "https://pub.dev"
source: hosted
version: "5.4.0"
version: "5.4.1"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: ddd72baa6f727e5b23f32d9af23d7d453d67946f380bd9c21daf474ee0f7326e
sha256: eb3afccfc452b2b2075acbe0c4b27de62dd596802b4e5e19869c1e926cbb20b3
url: "https://pub.dev"
source: hosted
version: "2.23.0"
version: "2.24.0"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: "758461f67b96aa5ad27625aaae39882fd6d1961b1c7e005301f9a74b6336100b"
sha256: c6711cf2f455532b84a94022c7aaf85088849763af2f01b775ca79d82d10a01a
url: "https://pub.dev"
source: hosted
version: "15.2.7"
version: "15.2.8"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: "614db1b0df0f53e541e41cc182b6d7ede5763c400f6ba232a5f8d0e1b5e5de32"
sha256: "1c9dacccb1aee1bf17ba519dda5563a16fdd2ec1e79b5f2e421cb4bf75a166f7"
url: "https://pub.dev"
source: hosted
version: "4.6.7"
version: "4.6.8"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: b5fbbcdd3e0e7f3fde72b0c119410f22737638fed5fc428b54bba06bc1455d81
sha256: "54317c26fa92f0d90a2017977ac791cb0504eca29fcf397f06adf727d4a7a2d5"
url: "https://pub.dev"
source: hosted
version: "3.10.7"
version: "3.10.8"
fixnum:
dependency: transitive
description:
@ -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:
@ -977,10 +1033,10 @@ packages:
dependency: "direct main"
description:
name: flutter_webrtc
sha256: dd47ca103b5b6217771e6277882674276d9621bbf9eb23da3c03898b507844e3
sha256: "792aa1e5838a719f29ae52c0773dbb5dd781fc33b1bf87c321b274e55ab51ad1"
url: "https://pub.dev"
source: hosted
version: "0.14.1"
version: "0.14.2"
font_awesome_flutter:
dependency: transitive
description:
@ -1093,6 +1149,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.15.6"
html2md:
dependency: "direct main"
description:
name: html2md
sha256: "465cf8ffa1b510fe0e97941579bf5b22e2d575f2cecb500a9c0254efe33a8036"
url: "https://pub.dev"
source: hosted
version: "1.3.2"
http:
dependency: transitive
description:
@ -1297,10 +1361,10 @@ packages:
dependency: "direct main"
description:
name: livekit_client
sha256: c270720a49b935591960c6f3296fd8f00c09b45a70cd64aef78cd0a8f8257913
sha256: "5d182f40cc9aafce60a9acf936bad8bc69010b5cbf0a949f6f27dc4390f2fcce"
url: "https://pub.dev"
source: hosted
version: "2.4.8"
version: "2.4.9"
local_auth:
dependency: "direct main"
description:
@ -1677,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:
@ -1689,10 +1785,10 @@ packages:
dependency: transitive
description:
name: posix
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
url: "https://pub.dev"
source: hosted
version: "6.0.2"
version: "6.0.3"
protobuf:
dependency: transitive
description:
@ -1809,10 +1905,10 @@ packages:
dependency: transitive
description:
name: record_web
sha256: "024c81eb7f51468b1833a3eca8b461c7ca25c04899dba37abe580bb57afd32e4"
sha256: a12856d0b3dd03d336b4b10d7520a8b3e21649a06a8f95815318feaa8f07adbb
url: "https://pub.dev"
source: hosted
version: "1.1.8"
version: "1.1.9"
record_windows:
dependency: transitive
description:

View File

@ -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+108
version: 3.0.0+110
environment:
sdk: ^3.7.2
@ -37,7 +37,7 @@ dependencies:
flutter_hooks: ^0.21.2
hooks_riverpod: ^2.6.1
bitsdojo_window: ^0.1.6
go_router: ^15.2.4
go_router: ^15.1.3
styled_widget: ^0.4.1
shared_preferences: ^2.5.3
flutter_riverpod: ^2.6.1
@ -123,10 +123,12 @@ dependencies:
receive_sharing_intent: ^1.8.1
top_snackbar_flutter: ^3.3.0
textfield_tags:
git:
git:
url: https://github.com/lionelmennig/textfield_tags.git
ref: fixes/allow-controller-re-registration
mime: ^2.0.0
html2md: ^1.3.2
flutter_typeahead: ^5.2.0
dev_dependencies:
flutter_test: