Compare commits
18 Commits
b89cffeb18
...
3.0.0+107
Author | SHA1 | Date | |
---|---|---|---|
825e6b5b6d | |||
2a3276973c | |||
f4e10afa8f | |||
60c5e584be | |||
2b237eaad9 | |||
891a0b999c | |||
01da729365 | |||
cef313b356 | |||
8bc8556f06 | |||
1a8abe5849 | |||
86258acc6e | |||
0062d3baf0 | |||
a7c9a2281c | |||
06e1623a86 | |||
434256e61e | |||
b275b8328d | |||
63230c16ff | |||
47c31ddec2 |
@ -1,3 +1,6 @@
|
||||
import java.util.Properties
|
||||
import java.io.FileInputStream
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
// START: FlutterFire Configuration
|
||||
@ -8,6 +11,12 @@ plugins {
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.solsynth.solian"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@ -31,23 +40,25 @@ android {
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias = keystoreProperties['keyAlias']
|
||||
keyPassword = keystoreProperties['keyPassword']
|
||||
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
|
||||
storePassword = keystoreProperties['storePassword']
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
minifyEnabled = true
|
||||
shrinkResources = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.google.android.material:material:1.12.0")
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleInstance"
|
||||
android:launchMode="singleTask"
|
||||
android:taskAffinity=""
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
@ -41,6 +41,18 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Share Intent Filters -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Sign in with Apple -->
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<style name="LaunchTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
@ -16,7 +16,7 @@
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<style name="NormalTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<style name="LaunchTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
@ -16,7 +16,7 @@
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<style name="NormalTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
File diff suppressed because one or more lines are too long
@ -48,6 +48,28 @@
|
||||
"deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.",
|
||||
"somethingWentWrong": "Something went wrong...",
|
||||
"deletePost": "Delete Post",
|
||||
"safetyReport": "Report",
|
||||
"safetyReportTitle": "Safety Report",
|
||||
"safetyReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",
|
||||
"safetyReportType": "Report Type",
|
||||
"safetyReportReason": "Additional Details",
|
||||
"safetyReportReasonHint": "Please provide more details about the issue...",
|
||||
"safetyReportSubmit": "Submit Report",
|
||||
"safetyReportSubmitting": "Submitting...",
|
||||
"safetyReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.",
|
||||
"safetyReportError": "Failed to submit report. Please try again.",
|
||||
"safetyReportReasonRequired": "Please provide details about the issue",
|
||||
"safetyReportTypeSpam": "Spam or Misleading",
|
||||
"safetyReportTypeHarassment": "Harassment or Abuse",
|
||||
"safetyReportTypeHateSpeech": "Hate Speech",
|
||||
"safetyReportTypeViolence": "Violence or Threats",
|
||||
"safetyReportTypeAdultContent": "Adult Content",
|
||||
"safetyReportTypeIntellectualProperty": "Intellectual Property Violation",
|
||||
"safetyReportTypeOther": "Other",
|
||||
"safetyReportTypeInappropriate": "Inappropriate Content",
|
||||
"safetyReportTypeCopyright": "Copyright Violation",
|
||||
"safetyReportSuccessTitle": "Report Submitted",
|
||||
"safetyReportErrorTitle": "Error",
|
||||
"deletePostHint": "Are you sure to delete this post?",
|
||||
"copyLink": "Copy Link",
|
||||
"postCreateAccountTitle": "Thanks for joining!",
|
||||
@ -410,6 +432,8 @@
|
||||
"articleDrafts": "Article drafts",
|
||||
"postDrafts": "Post drafts",
|
||||
"saveDraft": "Save draft",
|
||||
"draftSaved": "Draft saved",
|
||||
"draftSaveFailed": "Failed to save draft",
|
||||
"clearAllDrafts": "Clear All Drafts",
|
||||
"clearAllDraftsConfirm": "Are you sure you want to delete all drafts? This action cannot be undone.",
|
||||
"clearAll": "Clear All",
|
||||
@ -441,6 +465,7 @@
|
||||
"contactMethodDelete": "Delete Contact",
|
||||
"contactMethodNew": "New Contact Method",
|
||||
"contactMethodContentEmpty": "Contact content cannot be empty",
|
||||
"postContentEmpty": "Post content cannot be empty",
|
||||
"contactMethodVerificationSent": "Verification code sent to your contact method",
|
||||
"contactMethodVerificationNeeded": "The contact method is added, but not verified yet. You can verify it by tapping it and select verify.",
|
||||
"accountContactMethod": "Contact Methods",
|
||||
@ -540,5 +565,50 @@
|
||||
"orderId": "Order ID",
|
||||
"enterOrderId": "Enter your order ID",
|
||||
"restore": "Restore",
|
||||
"keyboardShortcuts": "Keyboard Shortcuts"
|
||||
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||
"share": "Share",
|
||||
"sharePost": "Share Post",
|
||||
"quickActions": "Quick Actions",
|
||||
"post": "Post",
|
||||
"copy": "Copy",
|
||||
"sendToChat": "Send to Chat",
|
||||
"failedToShareToPost": "Failed to share to post: {}",
|
||||
"shareToChatComingSoon": "Share to chat functionality coming soon",
|
||||
"failedToShareToChat": "Failed to share to chat: {}",
|
||||
"shareToSpecificChatComingSoon": "Share to {} coming soon",
|
||||
"directChat": "Direct Chat",
|
||||
"systemShareComingSoon": "System share functionality coming soon",
|
||||
"failedToShareToSystem": "Failed to share to system: {}",
|
||||
"failedToCopy": "Failed to copy: {}",
|
||||
"noChatRoomsAvailable": "No chat rooms available",
|
||||
"failedToLoadChats": "Failed to load chats",
|
||||
"contentToShare": "Content to share:",
|
||||
"unknownChat": "Unknown Chat",
|
||||
"addAdditionalMessage": "Add additional message...",
|
||||
"uploadingFiles": "Uploading files...",
|
||||
"sharedSuccessfully": "Shared successfully!",
|
||||
"navigateToChat": "Navigate to Chat",
|
||||
"wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?",
|
||||
"abuseReport": "Report",
|
||||
"abuseReportTitle": "Report Content",
|
||||
"abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.",
|
||||
"abuseReportType": "Report Type",
|
||||
"abuseReportReason": "Additional Details",
|
||||
"abuseReportReasonHint": "Please provide more details about the issue...",
|
||||
"abuseReportSubmit": "Submit Report",
|
||||
"abuseReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.",
|
||||
"abuseReportError": "Failed to submit report. Please try again.",
|
||||
"abuseReportReasonRequired": "Please provide details about the issue",
|
||||
"abuseReportSuccessTitle": "Report Submitted",
|
||||
"abuseReportErrorTitle": "Error",
|
||||
"abuseReportTypeSpam": "Spam or Misleading",
|
||||
"abuseReportTypeHarassment": "Harassment or Abuse",
|
||||
"abuseReportTypeInappropriate": "Inappropriate Content",
|
||||
"abuseReportTypeViolence": "Violence or Threats",
|
||||
"abuseReportTypeCopyright": "Copyright Violation",
|
||||
"abuseReportTypeImpersonation": "Impersonation",
|
||||
"abuseReportTypeOffensiveContent": "Offensive Content",
|
||||
"abuseReportTypePrivacyViolation": "Privacy Violation",
|
||||
"abuseReportTypeIllegalContent": "Illegal Content",
|
||||
"abuseReportTypeOther": "Other"
|
||||
}
|
||||
|
@ -319,5 +319,38 @@
|
||||
"processingPayment": "处理付款中...",
|
||||
"pleaseWait": "请稍候",
|
||||
"paymentFailed": "付款失败,请重试。",
|
||||
"paymentSuccess": "付款成功完成!"
|
||||
"paymentSuccess": "付款成功完成!",
|
||||
"drafts": "草稿",
|
||||
"noDrafts": "暂无草稿",
|
||||
"articleDrafts": "文章草稿",
|
||||
"postDrafts": "帖子草稿",
|
||||
"saveDraft": "保存草稿",
|
||||
"draftSaved": "草稿已保存",
|
||||
"draftSaveFailed": "保存草稿失败",
|
||||
"clearAllDrafts": "清空所有草稿",
|
||||
"clearAllDraftsConfirm": "确定要删除所有草稿吗?此操作无法撤销。",
|
||||
"clearAll": "清空全部",
|
||||
"untitled": "无标题",
|
||||
"noContent": "无内容",
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo": "{} 分钟前",
|
||||
"hoursAgo": "{} 小时前",
|
||||
"postContentEmpty": "帖子内容不能为空",
|
||||
"share": "分享",
|
||||
"quickActions": "快捷操作",
|
||||
"post": "帖子",
|
||||
"copy": "复制",
|
||||
"sendToChat": "发送到聊天",
|
||||
"failedToShareToPost": "分享到帖子失败:{}",
|
||||
"shareToChatComingSoon": "聊天分享功能即将推出",
|
||||
"failedToShareToChat": "分享到聊天失败:{}",
|
||||
"shareToSpecificChatComingSoon": "分享到 {} 即将推出",
|
||||
"directChat": "私聊",
|
||||
"systemShareComingSoon": "系统分享功能即将推出",
|
||||
"failedToShareToSystem": "系统分享失败:{}",
|
||||
"copiedToClipboard": "已复制到剪贴板",
|
||||
"failedToCopy": "复制失败:{}",
|
||||
"noChatRoomsAvailable": "没有可用的聊天室",
|
||||
"failedToLoadChats": "加载聊天失败",
|
||||
"unknownChat": "未知聊天"
|
||||
}
|
@ -334,5 +334,38 @@
|
||||
"membershipFeatureAllNova": "所有新星功能",
|
||||
"membershipFeatureExclusiveContent": "獨家內容",
|
||||
"membershipFeatureVipSupport": "VIP 支援",
|
||||
"membershipCurrentBadge": "目前"
|
||||
"membershipCurrentBadge": "目前",
|
||||
"drafts": "草稿",
|
||||
"noDrafts": "暫無草稿",
|
||||
"articleDrafts": "文章草稿",
|
||||
"postDrafts": "貼文草稿",
|
||||
"saveDraft": "儲存草稿",
|
||||
"draftSaved": "草稿已儲存",
|
||||
"draftSaveFailed": "儲存草稿失敗",
|
||||
"clearAllDrafts": "清空所有草稿",
|
||||
"clearAllDraftsConfirm": "確定要刪除所有草稿嗎?此操作無法復原。",
|
||||
"clearAll": "清空全部",
|
||||
"untitled": "無標題",
|
||||
"noContent": "無內容",
|
||||
"justNow": "剛剛",
|
||||
"minutesAgo": "{} 分鐘前",
|
||||
"hoursAgo": "{} 小時前",
|
||||
"postContentEmpty": "貼文內容不能為空",
|
||||
"share": "分享",
|
||||
"quickActions": "快速操作",
|
||||
"post": "貼文",
|
||||
"copy": "複製",
|
||||
"sendToChat": "傳送到聊天",
|
||||
"failedToShareToPost": "分享到貼文失敗:{}",
|
||||
"shareToChatComingSoon": "聊天分享功能即將推出",
|
||||
"failedToShareToChat": "分享到聊天失敗:{}",
|
||||
"shareToSpecificChatComingSoon": "分享到 {} 即將推出",
|
||||
"directChat": "私人聊天",
|
||||
"systemShareComingSoon": "系統分享功能即將推出",
|
||||
"failedToShareToSystem": "系統分享失敗:{}",
|
||||
"copiedToClipboard": "已複製到剪貼簿",
|
||||
"failedToCopy": "複製失敗:{}",
|
||||
"noChatRoomsAvailable": "沒有可用的聊天室",
|
||||
"failedToLoadChats": "載入聊天失敗",
|
||||
"unknownChat": "未知聊天"
|
||||
}
|
2
crowdin.yml
Normal file
2
crowdin.yml
Normal file
@ -0,0 +1,2 @@
|
||||
bundles:
|
||||
- 6
|
@ -44,6 +44,10 @@ target 'Runner' do
|
||||
pod 'Kingfisher', '~> 8.0'
|
||||
pod 'Alamofire'
|
||||
end
|
||||
|
||||
target 'SolianShareExtension' do
|
||||
inherit! :search_paths
|
||||
end
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
@ -156,12 +156,16 @@ PODS:
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- receive_sharing_intent (1.8.1):
|
||||
- Flutter
|
||||
- record_ios (1.0.0):
|
||||
- Flutter
|
||||
- SAMKeychain (1.5.3)
|
||||
- SDWebImage (5.21.1):
|
||||
- SDWebImage/Core (= 5.21.1)
|
||||
- SDWebImage/Core (5.21.1)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@ -231,7 +235,9 @@ 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`)
|
||||
- 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`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
@ -314,8 +320,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/pasteboard/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
receive_sharing_intent:
|
||||
:path: ".symlinks/plugins/receive_sharing_intent/ios"
|
||||
record_ios:
|
||||
:path: ".symlinks/plugins/record_ios/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sign_in_with_apple:
|
||||
@ -373,9 +383,11 @@ SPEC CHECKSUMS:
|
||||
pasteboard: 49088aeb6119d51f976a421db60d8e1ab079b63c
|
||||
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
|
||||
record_ios: fee1c924aa4879b882ebca2b4bce6011bcfc3d8b
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
SDWebImage: f29024626962457f3470184232766516dee8dfea
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sign_in_with_apple: c5dcc141574c8c54d5ac99dd2163c0c72ad22418
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
@ -388,6 +400,6 @@ SPEC CHECKSUMS:
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
WebRTC-SDK: dff00a3892bc570b6014e046297782084071657e
|
||||
|
||||
PODFILE CHECKSUM: 0c13198c20d0416ef589aeb2e1dac5c50262254f
|
||||
PODFILE CHECKSUM: f6df17c2a0cbd7af89692fd3877231eaea40230f
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
@ -10,6 +10,7 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
73D4264B2DEB815D006C0AAE /* NotifyDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
@ -18,6 +19,7 @@
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
B87C0E607033790E71B54D73 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F6D834CA86410B09796B312B /* Pods_Runner.framework */; };
|
||||
D174D53FF3E8EA943491A5CC /* Pods_SolianShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */; };
|
||||
D1772CE196985AE8E8C9F2E5 /* Pods_SolianNotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */; };
|
||||
E7A0B456EF7AAA71D1397081 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 9AE244813FCDFAA941430393 /* GoogleService-Info.plist */; };
|
||||
/* End PBXBuildFile section */
|
||||
@ -30,6 +32,13 @@
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
73C305D62E0BE878009035B9 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 73C305CD2E0BE878009035B9;
|
||||
remoteInfo = SolianShareExtension;
|
||||
};
|
||||
73CDD67F2DEC00480059D95D /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
@ -46,6 +55,7 @@
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
73C305D82E0BE878009035B9 /* SolianShareExtension.appex in Embed Foundation Extensions */,
|
||||
73CDD6812DEC00480059D95D /* SolianNotificationService.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
@ -68,9 +78,11 @@
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
14DFD79BE7C26E51B117583C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
192FDACE67D7CB6AED15C634 /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
1C14F71D23E4371602065522 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
252A83CE6862573BB856ED8E /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.release.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2D2457F8B2E6EF9C0F935035 /* Pods-NotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.profile.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
@ -79,11 +91,13 @@
|
||||
3A1C47BD29CC6AC2587D4DBE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
737E920B2DB6A9FF00BE9CDB /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = SolianNotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
73D4264A2DEB815D006C0AAE /* NotifyDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyDelegate.swift; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SolianShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.profile.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
@ -94,6 +108,7 @@
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
9AE244813FCDFAA941430393 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
A499FDB2082EB000933AA8C5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-SolianShareExtension/Pods-SolianShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SolianNotificationService.release.xcconfig"; path = "Target Support Files/Pods-SolianNotificationService/Pods-SolianNotificationService.release.xcconfig"; sourceTree = "<group>"; };
|
||||
E6B10A9A85BECA2E576C91FF /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
@ -102,6 +117,13 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
73C305DC2E0BE878009035B9 /* Exceptions for "SolianShareExtension" folder in "SolianShareExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 73C305CD2E0BE878009035B9 /* SolianShareExtension */;
|
||||
};
|
||||
73CDD6822DEC00480059D95D /* Exceptions for "SolianNotificationService" folder in "SolianNotificationService" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
@ -128,6 +150,14 @@
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
73C305CF2E0BE878009035B9 /* SolianShareExtension */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
73C305DC2E0BE878009035B9 /* Exceptions for "SolianShareExtension" folder in "SolianShareExtension" target */,
|
||||
);
|
||||
path = SolianShareExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
73CDD67B2DEC00480059D95D /* SolianNotificationService */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
@ -147,6 +177,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73C305CB2E0BE878009035B9 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D174D53FF3E8EA943491A5CC /* Pods_SolianShareExtension.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73CDD6772DEC00480059D95D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -181,6 +219,7 @@
|
||||
29812C17FFBE7DBBC7203981 /* Pods_RunnerTests.framework */,
|
||||
AA0CA8A3E15DEE023BB27438 /* Pods_NotificationService.framework */,
|
||||
39FE4CC6223F0D3C0E1FFD04 /* Pods_SolianNotificationService.framework */,
|
||||
7B40764A2C4CC0E7DC70A0D3 /* Pods_SolianShareExtension.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@ -200,6 +239,9 @@
|
||||
F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */,
|
||||
B93771F2A63E4148DC6142F7 /* Pods-SolianNotificationService.release.xcconfig */,
|
||||
8B40620B1EEBB09456406A3C /* Pods-SolianNotificationService.profile.xcconfig */,
|
||||
17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */,
|
||||
27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */,
|
||||
A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@ -221,6 +263,7 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
73CDD67B2DEC00480059D95D /* SolianNotificationService */,
|
||||
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
91E124CE95BCB4DCD890160D /* Pods */,
|
||||
@ -235,6 +278,7 @@
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
73CDD67A2DEC00480059D95D /* SolianNotificationService.appex */,
|
||||
73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@ -279,6 +323,27 @@
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
73C305CD2E0BE878009035B9 /* SolianShareExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 73C305DD2E0BE878009035B9 /* Build configuration list for PBXNativeTarget "SolianShareExtension" */;
|
||||
buildPhases = (
|
||||
67E069E457308C73A1C44EF4 /* [CP] Check Pods Manifest.lock */,
|
||||
73C305CA2E0BE878009035B9 /* Sources */,
|
||||
73C305CB2E0BE878009035B9 /* Frameworks */,
|
||||
73C305CC2E0BE878009035B9 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
73C305CF2E0BE878009035B9 /* SolianShareExtension */,
|
||||
);
|
||||
name = SolianShareExtension;
|
||||
productName = SolianShareExtension;
|
||||
productReference = 73C305CE2E0BE878009035B9 /* SolianShareExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
73CDD6792DEC00480059D95D /* SolianNotificationService */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 73CDD6832DEC00480059D95D /* Build configuration list for PBXNativeTarget "SolianNotificationService" */;
|
||||
@ -319,6 +384,7 @@
|
||||
);
|
||||
dependencies = (
|
||||
73CDD6802DEC00480059D95D /* PBXTargetDependency */,
|
||||
73C305D72E0BE878009035B9 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
73268D272DEB012A0076E970 /* Services */,
|
||||
@ -343,6 +409,9 @@
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
73C305CD2E0BE878009035B9 = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
};
|
||||
73CDD6792DEC00480059D95D = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
};
|
||||
@ -368,6 +437,7 @@
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
73CDD6792DEC00480059D95D /* SolianNotificationService */,
|
||||
73C305CD2E0BE878009035B9 /* SolianShareExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@ -380,6 +450,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73C305CC2E0BE878009035B9 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73CDD6782DEC00480059D95D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -479,6 +556,28 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
67E069E457308C73A1C44EF4 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-SolianShareExtension-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
8C0351B03869BBF493808288 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -544,6 +643,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73C305CA2E0BE878009035B9 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
73CDD6762DEC00480059D95D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@ -569,6 +675,11 @@
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
73C305D72E0BE878009035B9 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 73C305CD2E0BE878009035B9 /* SolianShareExtension */;
|
||||
targetProxy = 73C305D62E0BE878009035B9 /* PBXContainerItemProxy */;
|
||||
};
|
||||
73CDD6802DEC00480059D95D /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 73CDD6792DEC00480059D95D /* SolianNotificationService */;
|
||||
@ -656,6 +767,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -723,6 +835,129 @@
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
73C305D92E0BE878009035B9 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 17FAB080A9C53193ABD9C15B /* Pods-SolianShareExtension.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolianShareExtension/SolianShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
73C305DA2E0BE878009035B9 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 27C66EFB5A705F1A822C3EB0 /* Pods-SolianShareExtension.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolianShareExtension/SolianShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
73C305DB2E0BE878009035B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = A85FF612AE7623A9934E57CE /* Pods-SolianShareExtension.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolianShareExtension/SolianShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = SolianShareExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = SolianShareExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.5;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian.SolianShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
73CDD6842DEC00480059D95D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = F830F535CB92E3F2E1653A11 /* Pods-SolianNotificationService.debug.xcconfig */;
|
||||
@ -734,6 +969,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolianNotificationService/SolianNotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
@ -775,6 +1011,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolianNotificationService/SolianNotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
@ -813,6 +1050,7 @@
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = SolianNotificationService/SolianNotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
@ -959,6 +1197,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -985,6 +1224,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CUSTOM_GROUP_ID = group.solsynth.solian;
|
||||
DEVELOPMENT_TEAM = W7HPZ53V6B;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@ -1015,6 +1255,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
73C305DD2E0BE878009035B9 /* Build configuration list for PBXNativeTarget "SolianShareExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
73C305D92E0BE878009035B9 /* Debug */,
|
||||
73C305DA2E0BE878009035B9 /* Release */,
|
||||
73C305DB2E0BE878009035B9 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
73CDD6832DEC00480059D95D /* Build configuration list for PBXNativeTarget "SolianNotificationService" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
@ -34,6 +34,17 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCalendarsUsageDescription</key>
|
||||
@ -76,6 +87,8 @@
|
||||
</array>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>INStartCallIntent</string>
|
||||
|
@ -11,10 +11,15 @@
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>webcredentials:solian.app</string>
|
||||
<string>applinks:solian.app</string>
|
||||
</array>
|
||||
<key>com.apple.developer.device-information.user-assigned-device-name</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.usernotifications.communication</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.solsynth.solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.solsynth.solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
24
ios/SolianShareExtension/Base.lproj/MainInterface.storyboard
Normal file
24
ios/SolianShareExtension/Base.lproj/MainInterface.storyboard
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Share View Controller-->
|
||||
<scene sceneID="ceB-am-kn3">
|
||||
<objects>
|
||||
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
42
ios/SolianShareExtension/Info.plist
Normal file
42
ios/SolianShareExtension/Info.plist
Normal file
@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>AppGroupId</key>
|
||||
<string>$(CUSTOM_GROUP_ID)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>PHSupportedMediaTypes</key>
|
||||
<array>
|
||||
<!--TODO: Add this flag, if you want to support sharing video into your app-->
|
||||
<string>Video</string>
|
||||
<!--TODO: Add this flag, if you want to support sharing images into your app-->
|
||||
<string>Image</string>
|
||||
</array>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>100</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>100</integer>
|
||||
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
|
||||
<integer>100</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
<string>MainInterface</string>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
14
ios/SolianShareExtension/ShareViewController.swift
Normal file
14
ios/SolianShareExtension/ShareViewController.swift
Normal file
@ -0,0 +1,14 @@
|
||||
//
|
||||
// ShareViewController.swift
|
||||
// SolianShareExtension
|
||||
//
|
||||
// Created by LittleSheep on 2025/6/25.
|
||||
//
|
||||
|
||||
import receive_sharing_intent
|
||||
|
||||
class ShareViewController: RSIShareViewController {
|
||||
override func shouldAutoRedirect() -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
10
ios/SolianShareExtension/SolianShareExtension.entitlements
Normal file
10
ios/SolianShareExtension/SolianShareExtension.entitlements
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.solsynth.solian</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
@ -1,26 +1,10 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class ComposeDrafts extends Table {
|
||||
class PostDrafts extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get title => text().withDefault(const Constant(''))();
|
||||
TextColumn get description => text().withDefault(const Constant(''))();
|
||||
TextColumn get content => text().withDefault(const Constant(''))();
|
||||
TextColumn get attachmentIds => text().withDefault(const Constant('[]'))(); // JSON array as string
|
||||
IntColumn get visibility => integer().withDefault(const Constant(0))(); // 0=public, 1=unlisted, 2=friends, 3=selected, 4=private
|
||||
TextColumn get post => text()(); // Store SnPost model as JSON string
|
||||
DateTimeColumn get lastModified => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
class ArticleDrafts extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get title => text().withDefault(const Constant(''))();
|
||||
TextColumn get description => text().withDefault(const Constant(''))();
|
||||
TextColumn get content => text().withDefault(const Constant(''))();
|
||||
IntColumn get visibility => integer().withDefault(const Constant(0))(); // 0=public, 1=unlisted, 2=friends, 3=private
|
||||
DateTimeColumn get lastModified => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
@ -2,16 +2,17 @@ import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:island/database/message.dart';
|
||||
import 'package:island/database/draft.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
|
||||
part 'drift_db.g.dart';
|
||||
|
||||
// Define the database
|
||||
@DriftDatabase(tables: [ChatMessages, ComposeDrafts, ArticleDrafts])
|
||||
@DriftDatabase(tables: [ChatMessages, PostDrafts])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase(super.e);
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
int get schemaVersion => 4;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@ -23,10 +24,9 @@ class AppDatabase extends _$AppDatabase {
|
||||
// Add isRead column with default value false
|
||||
await m.addColumn(chatMessages, chatMessages.isRead);
|
||||
}
|
||||
if (from < 3) {
|
||||
// Add draft tables
|
||||
await m.createTable(composeDrafts);
|
||||
await m.createTable(articleDrafts);
|
||||
if (from < 4) {
|
||||
// Drop old draft tables if they exist
|
||||
await m.createTable(postDrafts);
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -98,51 +98,23 @@ class AppDatabase extends _$AppDatabase {
|
||||
);
|
||||
}
|
||||
|
||||
// Methods for compose drafts
|
||||
Future<List<ComposeDraft>> getAllComposeDrafts() {
|
||||
return (select(composeDrafts)
|
||||
..orderBy([(d) => OrderingTerm.desc(d.lastModified)]))
|
||||
.get();
|
||||
// Methods for post drafts
|
||||
Future<List<SnPost>> getAllPostDrafts() async {
|
||||
final drafts = await select(postDrafts).get();
|
||||
return drafts
|
||||
.map((draft) => SnPost.fromJson(jsonDecode(draft.post)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<ComposeDraft?> getComposeDraft(String id) {
|
||||
return (select(composeDrafts)..where((d) => d.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
Future<void> addPostDraft(PostDraftsCompanion entry) async {
|
||||
await into(postDrafts).insert(entry, mode: InsertMode.replace);
|
||||
}
|
||||
|
||||
Future<int> saveComposeDraft(ComposeDraftsCompanion draft) {
|
||||
return into(composeDrafts).insert(draft, mode: InsertMode.insertOrReplace);
|
||||
Future<void> deletePostDraft(String id) async {
|
||||
await (delete(postDrafts)..where((tbl) => tbl.id.equals(id))).go();
|
||||
}
|
||||
|
||||
Future<int> deleteComposeDraft(String id) {
|
||||
return (delete(composeDrafts)..where((d) => d.id.equals(id))).go();
|
||||
}
|
||||
|
||||
Future<int> clearAllComposeDrafts() {
|
||||
return delete(composeDrafts).go();
|
||||
}
|
||||
|
||||
// Methods for article drafts
|
||||
Future<List<ArticleDraft>> getAllArticleDrafts() {
|
||||
return (select(articleDrafts)
|
||||
..orderBy([(d) => OrderingTerm.desc(d.lastModified)]))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<ArticleDraft?> getArticleDraft(String id) {
|
||||
return (select(articleDrafts)..where((d) => d.id.equals(id)))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<int> saveArticleDraft(ArticleDraftsCompanion draft) {
|
||||
return into(articleDrafts).insert(draft, mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
|
||||
Future<int> deleteArticleDraft(String id) {
|
||||
return (delete(articleDrafts)..where((d) => d.id.equals(id))).go();
|
||||
}
|
||||
|
||||
Future<int> clearAllArticleDrafts() {
|
||||
return delete(articleDrafts).go();
|
||||
Future<void> clearAllPostDrafts() async {
|
||||
await delete(postDrafts).go();
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -18,6 +18,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/screens/tabs.dart';
|
||||
import 'package:island/services/notify.dart';
|
||||
import 'package:island/services/timezone.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
@ -60,11 +61,11 @@ void main() async {
|
||||
if (!kIsWeb && (Platform.isMacOS || Platform.isLinux || Platform.isWindows)) {
|
||||
doWhenWindowReady(() {
|
||||
const defaultSize = Size(360, 640);
|
||||
|
||||
|
||||
// Get saved window size from preferences
|
||||
final savedSizeString = prefs.getString(kAppWindowSize);
|
||||
Size initialSize = defaultSize;
|
||||
|
||||
|
||||
if (savedSizeString != null) {
|
||||
try {
|
||||
final parts = savedSizeString.split(',');
|
||||
@ -78,12 +79,14 @@ void main() async {
|
||||
initialSize = defaultSize;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
appWindow.minSize = defaultSize;
|
||||
appWindow.size = initialSize;
|
||||
appWindow.alignment = Alignment.center;
|
||||
appWindow.show();
|
||||
log("[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}");
|
||||
log(
|
||||
"[SplashScreen] Desktop window is ready with size: ${initialSize.width}x${initialSize.height}",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -124,6 +127,8 @@ void main() async {
|
||||
|
||||
final appRouter = AppRouter();
|
||||
|
||||
final globalOverlay = GlobalKey<OverlayState>();
|
||||
|
||||
class IslandApp extends HookConsumerWidget {
|
||||
const IslandApp({super.key});
|
||||
|
||||
@ -182,7 +187,16 @@ class IslandApp extends HookConsumerWidget {
|
||||
theme: theme?.light,
|
||||
darkTheme: theme?.dark,
|
||||
themeMode: ThemeMode.system,
|
||||
routerConfig: appRouter.config(),
|
||||
routerConfig: appRouter.config(
|
||||
navigatorObservers:
|
||||
() => [
|
||||
TabNavigationObserver(
|
||||
onChange: (route) {
|
||||
ref.read(currentRouteProvider.notifier).state = route;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
supportedLocales: context.supportedLocales,
|
||||
localizationsDelegates: [
|
||||
...context.localizationDelegates,
|
||||
@ -192,6 +206,7 @@ class IslandApp extends HookConsumerWidget {
|
||||
locale: context.locale,
|
||||
builder: (context, child) {
|
||||
return Overlay(
|
||||
key: globalOverlay,
|
||||
initialEntries: [
|
||||
OverlayEntry(
|
||||
builder:
|
||||
|
@ -21,3 +21,22 @@ sealed class SnEmbedLink with _$SnEmbedLink {
|
||||
factory SnEmbedLink.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnEmbedLinkFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnScrappedLink with _$SnScrappedLink {
|
||||
const factory SnScrappedLink({
|
||||
required String type,
|
||||
required String url,
|
||||
required String title,
|
||||
required String? description,
|
||||
required String? imageUrl,
|
||||
required String faviconUrl,
|
||||
required String siteName,
|
||||
required String? contentType,
|
||||
required String? author,
|
||||
required DateTime? publishedDate,
|
||||
}) = _SnScrappedLink;
|
||||
|
||||
factory SnScrappedLink.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnScrappedLinkFromJson(json);
|
||||
}
|
||||
|
@ -170,6 +170,166 @@ as DateTime?,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnScrappedLink {
|
||||
|
||||
String get type; String get url; String get title; String? get description; String? get imageUrl; String get faviconUrl; String get siteName; String? get contentType; String? get author; DateTime? get publishedDate;
|
||||
/// Create a copy of SnScrappedLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnScrappedLinkCopyWith<SnScrappedLink> get copyWith => _$SnScrappedLinkCopyWithImpl<SnScrappedLink>(this as SnScrappedLink, _$identity);
|
||||
|
||||
/// Serializes this SnScrappedLink to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnScrappedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnScrappedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnScrappedLinkCopyWith<$Res> {
|
||||
factory $SnScrappedLinkCopyWith(SnScrappedLink value, $Res Function(SnScrappedLink) _then) = _$SnScrappedLinkCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnScrappedLinkCopyWithImpl<$Res>
|
||||
implements $SnScrappedLinkCopyWith<$Res> {
|
||||
_$SnScrappedLinkCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnScrappedLink _self;
|
||||
final $Res Function(SnScrappedLink) _then;
|
||||
|
||||
/// Create a copy of SnScrappedLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type ? _self.type : type // 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?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
|
||||
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
|
||||
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnScrappedLink implements SnScrappedLink {
|
||||
const _SnScrappedLink({required this.type, required this.url, required this.title, required this.description, required this.imageUrl, required this.faviconUrl, required this.siteName, required this.contentType, required this.author, required this.publishedDate});
|
||||
factory _SnScrappedLink.fromJson(Map<String, dynamic> json) => _$SnScrappedLinkFromJson(json);
|
||||
|
||||
@override final String type;
|
||||
@override final String url;
|
||||
@override final String title;
|
||||
@override final String? description;
|
||||
@override final String? imageUrl;
|
||||
@override final String faviconUrl;
|
||||
@override final String siteName;
|
||||
@override final String? contentType;
|
||||
@override final String? author;
|
||||
@override final DateTime? publishedDate;
|
||||
|
||||
/// Create a copy of SnScrappedLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnScrappedLinkCopyWith<_SnScrappedLink> get copyWith => __$SnScrappedLinkCopyWithImpl<_SnScrappedLink>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnScrappedLinkToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnScrappedLink&&(identical(other.type, type) || other.type == type)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.imageUrl, imageUrl) || other.imageUrl == imageUrl)&&(identical(other.faviconUrl, faviconUrl) || other.faviconUrl == faviconUrl)&&(identical(other.siteName, siteName) || other.siteName == siteName)&&(identical(other.contentType, contentType) || other.contentType == contentType)&&(identical(other.author, author) || other.author == author)&&(identical(other.publishedDate, publishedDate) || other.publishedDate == publishedDate));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,url,title,description,imageUrl,faviconUrl,siteName,contentType,author,publishedDate);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnScrappedLink(type: $type, url: $url, title: $title, description: $description, imageUrl: $imageUrl, faviconUrl: $faviconUrl, siteName: $siteName, contentType: $contentType, author: $author, publishedDate: $publishedDate)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnScrappedLinkCopyWith<$Res> implements $SnScrappedLinkCopyWith<$Res> {
|
||||
factory _$SnScrappedLinkCopyWith(_SnScrappedLink value, $Res Function(_SnScrappedLink) _then) = __$SnScrappedLinkCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String type, String url, String title, String? description, String? imageUrl, String faviconUrl, String siteName, String? contentType, String? author, DateTime? publishedDate
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnScrappedLinkCopyWithImpl<$Res>
|
||||
implements _$SnScrappedLinkCopyWith<$Res> {
|
||||
__$SnScrappedLinkCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnScrappedLink _self;
|
||||
final $Res Function(_SnScrappedLink) _then;
|
||||
|
||||
/// Create a copy of SnScrappedLink
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? url = null,Object? title = null,Object? description = freezed,Object? imageUrl = freezed,Object? faviconUrl = null,Object? siteName = null,Object? contentType = freezed,Object? author = freezed,Object? publishedDate = freezed,}) {
|
||||
return _then(_SnScrappedLink(
|
||||
type: null == type ? _self.type : type // 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?,imageUrl: freezed == imageUrl ? _self.imageUrl : imageUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,faviconUrl: null == faviconUrl ? _self.faviconUrl : faviconUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String,siteName: null == siteName ? _self.siteName : siteName // ignore: cast_nullable_to_non_nullable
|
||||
as String,contentType: freezed == contentType ? _self.contentType : contentType // ignore: cast_nullable_to_non_nullable
|
||||
as String?,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
|
||||
as String?,publishedDate: freezed == publishedDate ? _self.publishedDate : publishedDate // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
@ -35,3 +35,34 @@ Map<String, dynamic> _$SnEmbedLinkToJson(_SnEmbedLink instance) =>
|
||||
'Author': instance.author,
|
||||
'PublishedDate': instance.publishedDate?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnScrappedLink _$SnScrappedLinkFromJson(Map<String, dynamic> json) =>
|
||||
_SnScrappedLink(
|
||||
type: json['type'] as String,
|
||||
url: json['url'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String?,
|
||||
imageUrl: json['image_url'] as String?,
|
||||
faviconUrl: json['favicon_url'] as String,
|
||||
siteName: json['site_name'] as String,
|
||||
contentType: json['content_type'] as String?,
|
||||
author: json['author'] as String?,
|
||||
publishedDate:
|
||||
json['published_date'] == null
|
||||
? null
|
||||
: DateTime.parse(json['published_date'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnScrappedLinkToJson(_SnScrappedLink instance) =>
|
||||
<String, dynamic>{
|
||||
'type': instance.type,
|
||||
'url': instance.url,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'image_url': instance.imageUrl,
|
||||
'favicon_url': instance.faviconUrl,
|
||||
'site_name': instance.siteName,
|
||||
'content_type': instance.contentType,
|
||||
'author': instance.author,
|
||||
'published_date': instance.publishedDate?.toIso8601String(),
|
||||
};
|
||||
|
@ -9,36 +9,36 @@ part 'post.g.dart';
|
||||
sealed class SnPost with _$SnPost {
|
||||
const factory SnPost({
|
||||
required String id,
|
||||
required String? title,
|
||||
required String? description,
|
||||
required String? language,
|
||||
required DateTime? editedAt,
|
||||
required DateTime publishedAt,
|
||||
required int visibility,
|
||||
required String? content,
|
||||
required int type,
|
||||
required Map<String, dynamic>? meta,
|
||||
required int viewsUnique,
|
||||
required int viewsTotal,
|
||||
required int upvotes,
|
||||
required int downvotes,
|
||||
required int repliesCount,
|
||||
required String? threadedPostId,
|
||||
required SnPost? threadedPost,
|
||||
required String? repliedPostId,
|
||||
required SnPost? repliedPost,
|
||||
required String? forwardedPostId,
|
||||
required SnPost? forwardedPost,
|
||||
required List<SnCloudFile> attachments,
|
||||
required SnPublisher publisher,
|
||||
String? title,
|
||||
String? description,
|
||||
String? language,
|
||||
DateTime? editedAt,
|
||||
@Default(null) DateTime? publishedAt,
|
||||
@Default(0) int visibility,
|
||||
String? content,
|
||||
@Default(0) int type,
|
||||
Map<String, dynamic>? meta,
|
||||
@Default(0) int viewsUnique,
|
||||
@Default(0) int viewsTotal,
|
||||
@Default(0) int upvotes,
|
||||
@Default(0) int downvotes,
|
||||
@Default(0) int repliesCount,
|
||||
String? threadedPostId,
|
||||
SnPost? threadedPost,
|
||||
String? repliedPostId,
|
||||
SnPost? repliedPost,
|
||||
String? forwardedPostId,
|
||||
SnPost? forwardedPost,
|
||||
@Default([]) List<SnCloudFile> attachments,
|
||||
@Default(SnPublisher()) SnPublisher publisher,
|
||||
@Default({}) Map<String, int> reactionsCount,
|
||||
required List<dynamic> reactions,
|
||||
required List<dynamic> tags,
|
||||
required List<dynamic> categories,
|
||||
required List<dynamic> collections,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
@Default([]) List<dynamic> reactions,
|
||||
@Default([]) List<dynamic> tags,
|
||||
@Default([]) List<dynamic> categories,
|
||||
@Default([]) List<dynamic> collections,
|
||||
@Default(null) DateTime? createdAt,
|
||||
@Default(null) DateTime? updatedAt,
|
||||
DateTime? deletedAt,
|
||||
@Default(false) bool isTruncated,
|
||||
}) = _SnPost;
|
||||
|
||||
@ -48,20 +48,20 @@ sealed class SnPost with _$SnPost {
|
||||
@freezed
|
||||
sealed class SnPublisher with _$SnPublisher {
|
||||
const factory SnPublisher({
|
||||
required String id,
|
||||
required int type,
|
||||
required String name,
|
||||
required String nick,
|
||||
@Default('') String id,
|
||||
@Default(0) int type,
|
||||
@Default('') String name,
|
||||
@Default('') String nick,
|
||||
@Default('') String bio,
|
||||
required SnCloudFile? picture,
|
||||
required SnCloudFile? background,
|
||||
required SnAccount? account,
|
||||
required String? accountId,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
required DateTime? deletedAt,
|
||||
required String? realmId,
|
||||
required SnVerificationMark? verification,
|
||||
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) =>
|
||||
|
@ -16,7 +16,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$SnPost {
|
||||
|
||||
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<dynamic> get tags; List<dynamic> get categories; List<dynamic> get collections; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt; bool get isTruncated;
|
||||
String get id; String? get title; String? get description; String? get language; DateTime? get editedAt; DateTime? get publishedAt; int get visibility; String? get content; int get type; Map<String, dynamic>? get meta; int get viewsUnique; int get viewsTotal; int get upvotes; int get downvotes; int get repliesCount; String? get threadedPostId; SnPost? get threadedPost; String? get repliedPostId; SnPost? get repliedPost; String? get forwardedPostId; SnPost? get forwardedPost; List<SnCloudFile> get attachments; SnPublisher get publisher; Map<String, int> get reactionsCount; List<dynamic> get reactions; List<dynamic> get tags; List<dynamic> get categories; List<dynamic> get collections; DateTime? get createdAt; DateTime? get updatedAt; DateTime? get deletedAt; bool get isTruncated;
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@ -49,7 +49,7 @@ abstract mixin class $SnPostCopyWith<$Res> {
|
||||
factory $SnPostCopyWith(SnPost value, $Res Function(SnPost) _then) = _$SnPostCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, bool isTruncated
|
||||
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
|
||||
});
|
||||
|
||||
|
||||
@ -66,15 +66,15 @@ class _$SnPostCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? isTruncated = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: freezed == 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?,language: freezed == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
|
||||
as String?,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,publishedAt: null == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
|
||||
as int,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as int,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
@ -96,9 +96,9 @@ as Map<String, int>,reactions: null == reactions ? _self.reactions : reactions /
|
||||
as List<dynamic>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,categories: null == categories ? _self.categories : categories // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,collections: null == collections ? _self.collections : collections // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,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 List<dynamic>,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?,isTruncated: null == isTruncated ? _self.isTruncated : isTruncated // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
@ -156,7 +156,7 @@ $SnPublisherCopyWith<$Res> get publisher {
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnPost implements SnPost {
|
||||
const _SnPost({required this.id, required this.title, required this.description, required this.language, required this.editedAt, required this.publishedAt, required this.visibility, required this.content, required this.type, required final Map<String, dynamic>? meta, required this.viewsUnique, required this.viewsTotal, required this.upvotes, required this.downvotes, required this.repliesCount, required this.threadedPostId, required this.threadedPost, required this.repliedPostId, required this.repliedPost, required this.forwardedPostId, required this.forwardedPost, required final List<SnCloudFile> attachments, required this.publisher, final Map<String, int> reactionsCount = const {}, required final List<dynamic> reactions, required final List<dynamic> tags, required final List<dynamic> categories, required final List<dynamic> collections, required this.createdAt, required this.updatedAt, required 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 [], this.publisher = const SnPublisher(), final Map<String, int> reactionsCount = const {}, final List<dynamic> reactions = const [], final List<dynamic> tags = const [], final List<dynamic> 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;
|
||||
@ -164,10 +164,10 @@ class _SnPost implements SnPost {
|
||||
@override final String? description;
|
||||
@override final String? language;
|
||||
@override final DateTime? editedAt;
|
||||
@override final DateTime publishedAt;
|
||||
@override final int visibility;
|
||||
@override@JsonKey() final DateTime? publishedAt;
|
||||
@override@JsonKey() final int visibility;
|
||||
@override final String? content;
|
||||
@override final int type;
|
||||
@override@JsonKey() final int type;
|
||||
final Map<String, dynamic>? _meta;
|
||||
@override Map<String, dynamic>? get meta {
|
||||
final value = _meta;
|
||||
@ -177,11 +177,11 @@ class _SnPost implements SnPost {
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final int viewsUnique;
|
||||
@override final int viewsTotal;
|
||||
@override final int upvotes;
|
||||
@override final int downvotes;
|
||||
@override final int repliesCount;
|
||||
@override@JsonKey() final int viewsUnique;
|
||||
@override@JsonKey() final int viewsTotal;
|
||||
@override@JsonKey() final int upvotes;
|
||||
@override@JsonKey() final int downvotes;
|
||||
@override@JsonKey() final int repliesCount;
|
||||
@override final String? threadedPostId;
|
||||
@override final SnPost? threadedPost;
|
||||
@override final String? repliedPostId;
|
||||
@ -189,13 +189,13 @@ class _SnPost implements SnPost {
|
||||
@override final String? forwardedPostId;
|
||||
@override final SnPost? forwardedPost;
|
||||
final List<SnCloudFile> _attachments;
|
||||
@override List<SnCloudFile> get attachments {
|
||||
@override@JsonKey() List<SnCloudFile> get attachments {
|
||||
if (_attachments is EqualUnmodifiableListView) return _attachments;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_attachments);
|
||||
}
|
||||
|
||||
@override final SnPublisher publisher;
|
||||
@override@JsonKey() final SnPublisher publisher;
|
||||
final Map<String, int> _reactionsCount;
|
||||
@override@JsonKey() Map<String, int> get reactionsCount {
|
||||
if (_reactionsCount is EqualUnmodifiableMapView) return _reactionsCount;
|
||||
@ -204,35 +204,35 @@ class _SnPost implements SnPost {
|
||||
}
|
||||
|
||||
final List<dynamic> _reactions;
|
||||
@override List<dynamic> get reactions {
|
||||
@override@JsonKey() List<dynamic> get reactions {
|
||||
if (_reactions is EqualUnmodifiableListView) return _reactions;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_reactions);
|
||||
}
|
||||
|
||||
final List<dynamic> _tags;
|
||||
@override List<dynamic> get tags {
|
||||
@override@JsonKey() List<dynamic> get tags {
|
||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_tags);
|
||||
}
|
||||
|
||||
final List<dynamic> _categories;
|
||||
@override List<dynamic> get categories {
|
||||
@override@JsonKey() List<dynamic> get categories {
|
||||
if (_categories is EqualUnmodifiableListView) return _categories;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_categories);
|
||||
}
|
||||
|
||||
final List<dynamic> _collections;
|
||||
@override List<dynamic> get collections {
|
||||
@override@JsonKey() List<dynamic> get collections {
|
||||
if (_collections is EqualUnmodifiableListView) return _collections;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_collections);
|
||||
}
|
||||
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override@JsonKey() final DateTime? createdAt;
|
||||
@override@JsonKey() final DateTime? updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
@override@JsonKey() final bool isTruncated;
|
||||
|
||||
@ -269,7 +269,7 @@ abstract mixin class _$SnPostCopyWith<$Res> implements $SnPostCopyWith<$Res> {
|
||||
factory _$SnPostCopyWith(_SnPost value, $Res Function(_SnPost) _then) = __$SnPostCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt, bool isTruncated
|
||||
String id, String? title, String? description, String? language, DateTime? editedAt, DateTime? publishedAt, int visibility, String? content, int type, Map<String, dynamic>? meta, int viewsUnique, int viewsTotal, int upvotes, int downvotes, int repliesCount, String? threadedPostId, SnPost? threadedPost, String? repliedPostId, SnPost? repliedPost, String? forwardedPostId, SnPost? forwardedPost, List<SnCloudFile> attachments, SnPublisher publisher, Map<String, int> reactionsCount, List<dynamic> reactions, List<dynamic> tags, List<dynamic> categories, List<dynamic> collections, DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, bool isTruncated
|
||||
});
|
||||
|
||||
|
||||
@ -286,15 +286,15 @@ class __$SnPostCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of SnPost
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = null,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? isTruncated = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = freezed,Object? description = freezed,Object? language = freezed,Object? editedAt = freezed,Object? publishedAt = freezed,Object? visibility = null,Object? content = freezed,Object? type = null,Object? meta = freezed,Object? viewsUnique = null,Object? viewsTotal = null,Object? upvotes = null,Object? downvotes = null,Object? repliesCount = null,Object? threadedPostId = freezed,Object? threadedPost = freezed,Object? repliedPostId = freezed,Object? repliedPost = freezed,Object? forwardedPostId = freezed,Object? forwardedPost = freezed,Object? attachments = null,Object? publisher = null,Object? reactionsCount = null,Object? reactions = null,Object? tags = null,Object? categories = null,Object? collections = null,Object? createdAt = freezed,Object? updatedAt = freezed,Object? deletedAt = freezed,Object? isTruncated = null,}) {
|
||||
return _then(_SnPost(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: freezed == 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?,language: freezed == language ? _self.language : language // ignore: cast_nullable_to_non_nullable
|
||||
as String?,editedAt: freezed == editedAt ? _self.editedAt : editedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,publishedAt: null == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,visibility: null == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
|
||||
as int,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as int,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
@ -316,9 +316,9 @@ as Map<String, int>,reactions: null == reactions ? _self._reactions : reactions
|
||||
as List<dynamic>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,categories: null == categories ? _self._categories : categories // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,collections: null == collections ? _self._collections : collections // ignore: cast_nullable_to_non_nullable
|
||||
as List<dynamic>,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 List<dynamic>,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?,isTruncated: null == isTruncated ? _self.isTruncated : isTruncated // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
@ -376,7 +376,7 @@ $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;
|
||||
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)
|
||||
@ -409,7 +409,7 @@ 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
|
||||
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
|
||||
});
|
||||
|
||||
|
||||
@ -426,7 +426,7 @@ class _$SnPublisherCopyWithImpl<$Res>
|
||||
|
||||
/// 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 = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
|
||||
@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
|
||||
@ -437,9 +437,9 @@ as String,picture: freezed == picture ? _self.picture : picture // ignore: cast_
|
||||
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: 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 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?,
|
||||
@ -501,20 +501,20 @@ $SnVerificationMarkCopyWith<$Res>? get verification {
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnPublisher implements SnPublisher {
|
||||
const _SnPublisher({required this.id, required this.type, required this.name, required this.nick, this.bio = '', required this.picture, required this.background, required this.account, required this.accountId, required this.createdAt, required this.updatedAt, required this.deletedAt, required this.realmId, required this.verification});
|
||||
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 final String id;
|
||||
@override final int type;
|
||||
@override final String name;
|
||||
@override final String nick;
|
||||
@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 final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override@JsonKey() final DateTime? createdAt;
|
||||
@override@JsonKey() final DateTime? updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
@override final String? realmId;
|
||||
@override final SnVerificationMark? verification;
|
||||
@ -552,7 +552,7 @@ abstract mixin class _$SnPublisherCopyWith<$Res> implements $SnPublisherCopyWith
|
||||
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
|
||||
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
|
||||
});
|
||||
|
||||
|
||||
@ -569,7 +569,7 @@ class __$SnPublisherCopyWithImpl<$Res>
|
||||
|
||||
/// 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 = null,Object? updatedAt = null,Object? deletedAt = freezed,Object? realmId = freezed,Object? verification = freezed,}) {
|
||||
@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
|
||||
@ -580,9 +580,9 @@ as String,picture: freezed == picture ? _self.picture : picture // ignore: cast_
|
||||
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: 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 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?,
|
||||
|
@ -15,16 +15,19 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
|
||||
json['edited_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['edited_at'] as String),
|
||||
publishedAt: DateTime.parse(json['published_at'] as String),
|
||||
visibility: (json['visibility'] as num).toInt(),
|
||||
publishedAt:
|
||||
json['published_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['published_at'] as String),
|
||||
visibility: (json['visibility'] as num?)?.toInt() ?? 0,
|
||||
content: json['content'] as String?,
|
||||
type: (json['type'] as num).toInt(),
|
||||
type: (json['type'] as num?)?.toInt() ?? 0,
|
||||
meta: json['meta'] as Map<String, dynamic>?,
|
||||
viewsUnique: (json['views_unique'] as num).toInt(),
|
||||
viewsTotal: (json['views_total'] as num).toInt(),
|
||||
upvotes: (json['upvotes'] as num).toInt(),
|
||||
downvotes: (json['downvotes'] as num).toInt(),
|
||||
repliesCount: (json['replies_count'] as num).toInt(),
|
||||
viewsUnique: (json['views_unique'] as num?)?.toInt() ?? 0,
|
||||
viewsTotal: (json['views_total'] as num?)?.toInt() ?? 0,
|
||||
upvotes: (json['upvotes'] as num?)?.toInt() ?? 0,
|
||||
downvotes: (json['downvotes'] as num?)?.toInt() ?? 0,
|
||||
repliesCount: (json['replies_count'] as num?)?.toInt() ?? 0,
|
||||
threadedPostId: json['threaded_post_id'] as String?,
|
||||
threadedPost:
|
||||
json['threaded_post'] == null
|
||||
@ -41,21 +44,31 @@ _SnPost _$SnPostFromJson(Map<String, dynamic> json) => _SnPost(
|
||||
? null
|
||||
: SnPost.fromJson(json['forwarded_post'] as Map<String, dynamic>),
|
||||
attachments:
|
||||
(json['attachments'] as List<dynamic>)
|
||||
.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
publisher: SnPublisher.fromJson(json['publisher'] as Map<String, dynamic>),
|
||||
(json['attachments'] as List<dynamic>?)
|
||||
?.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>),
|
||||
reactionsCount:
|
||||
(json['reactions_count'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, (e as num).toInt()),
|
||||
) ??
|
||||
const {},
|
||||
reactions: json['reactions'] as List<dynamic>,
|
||||
tags: json['tags'] as List<dynamic>,
|
||||
categories: json['categories'] as List<dynamic>,
|
||||
collections: json['collections'] as List<dynamic>,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
reactions: json['reactions'] as List<dynamic>? ?? const [],
|
||||
tags: json['tags'] as List<dynamic>? ?? const [],
|
||||
categories: json['categories'] as List<dynamic>? ?? const [],
|
||||
collections: json['collections'] as List<dynamic>? ?? const [],
|
||||
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
|
||||
@ -69,7 +82,7 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||
'description': instance.description,
|
||||
'language': instance.language,
|
||||
'edited_at': instance.editedAt?.toIso8601String(),
|
||||
'published_at': instance.publishedAt.toIso8601String(),
|
||||
'published_at': instance.publishedAt?.toIso8601String(),
|
||||
'visibility': instance.visibility,
|
||||
'content': instance.content,
|
||||
'type': instance.type,
|
||||
@ -92,17 +105,17 @@ Map<String, dynamic> _$SnPostToJson(_SnPost instance) => <String, dynamic>{
|
||||
'tags': instance.tags,
|
||||
'categories': instance.categories,
|
||||
'collections': instance.collections,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'is_truncated': instance.isTruncated,
|
||||
};
|
||||
|
||||
_SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
|
||||
id: json['id'] as String,
|
||||
type: (json['type'] as num).toInt(),
|
||||
name: json['name'] as String,
|
||||
nick: json['nick'] as String,
|
||||
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
|
||||
@ -117,8 +130,14 @@ _SnPublisher _$SnPublisherFromJson(Map<String, dynamic> json) => _SnPublisher(
|
||||
? null
|
||||
: SnAccount.fromJson(json['account'] as Map<String, dynamic>),
|
||||
accountId: json['account_id'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] 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
|
||||
@ -143,8 +162,8 @@ Map<String, dynamic> _$SnPublisherToJson(_SnPublisher instance) =>
|
||||
'background': instance.background?.toJson(),
|
||||
'account': instance.account?.toJson(),
|
||||
'account_id': instance.accountId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
'updated_at': instance.updatedAt?.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
'realm_id': instance.realmId,
|
||||
'verification': instance.verification?.toJson(),
|
||||
|
28
lib/pods/link_preview.dart
Normal file
28
lib/pods/link_preview.dart
Normal file
@ -0,0 +1,28 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/models/embed.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
|
||||
part 'link_preview.g.dart';
|
||||
|
||||
@riverpod
|
||||
class LinkPreview extends _$LinkPreview {
|
||||
@override
|
||||
Future<SnScrappedLink?> build(String url) async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
try {
|
||||
final response = await client.get(
|
||||
'/scrap/link',
|
||||
queryParameters: {'url': url},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
return SnScrappedLink.fromJson(response.data);
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
// Return null on error to show fallback UI
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
164
lib/pods/link_preview.g.dart
Normal file
164
lib/pods/link_preview.g.dart
Normal file
@ -0,0 +1,164 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'link_preview.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$linkPreviewHash() => r'5130593d3066155cb958d20714ee577df1f940d7';
|
||||
|
||||
/// 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 _$LinkPreview
|
||||
extends BuildlessAutoDisposeAsyncNotifier<SnScrappedLink?> {
|
||||
late final String url;
|
||||
|
||||
FutureOr<SnScrappedLink?> build(String url);
|
||||
}
|
||||
|
||||
/// See also [LinkPreview].
|
||||
@ProviderFor(LinkPreview)
|
||||
const linkPreviewProvider = LinkPreviewFamily();
|
||||
|
||||
/// See also [LinkPreview].
|
||||
class LinkPreviewFamily extends Family<AsyncValue<SnScrappedLink?>> {
|
||||
/// See also [LinkPreview].
|
||||
const LinkPreviewFamily();
|
||||
|
||||
/// See also [LinkPreview].
|
||||
LinkPreviewProvider call(String url) {
|
||||
return LinkPreviewProvider(url);
|
||||
}
|
||||
|
||||
@override
|
||||
LinkPreviewProvider getProviderOverride(
|
||||
covariant LinkPreviewProvider provider,
|
||||
) {
|
||||
return call(provider.url);
|
||||
}
|
||||
|
||||
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'linkPreviewProvider';
|
||||
}
|
||||
|
||||
/// See also [LinkPreview].
|
||||
class LinkPreviewProvider
|
||||
extends AutoDisposeAsyncNotifierProviderImpl<LinkPreview, SnScrappedLink?> {
|
||||
/// See also [LinkPreview].
|
||||
LinkPreviewProvider(String url)
|
||||
: this._internal(
|
||||
() => LinkPreview()..url = url,
|
||||
from: linkPreviewProvider,
|
||||
name: r'linkPreviewProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$linkPreviewHash,
|
||||
dependencies: LinkPreviewFamily._dependencies,
|
||||
allTransitiveDependencies: LinkPreviewFamily._allTransitiveDependencies,
|
||||
url: url,
|
||||
);
|
||||
|
||||
LinkPreviewProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.url,
|
||||
}) : super.internal();
|
||||
|
||||
final String url;
|
||||
|
||||
@override
|
||||
FutureOr<SnScrappedLink?> runNotifierBuild(covariant LinkPreview notifier) {
|
||||
return notifier.build(url);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(LinkPreview Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: LinkPreviewProvider._internal(
|
||||
() => create()..url = url,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
url: url,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<LinkPreview, SnScrappedLink?>
|
||||
createElement() {
|
||||
return _LinkPreviewProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is LinkPreviewProvider && other.url == url;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, url.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin LinkPreviewRef on AutoDisposeAsyncNotifierProviderRef<SnScrappedLink?> {
|
||||
/// The parameter `url` of this provider.
|
||||
String get url;
|
||||
}
|
||||
|
||||
class _LinkPreviewProviderElement
|
||||
extends
|
||||
AutoDisposeAsyncNotifierProviderElement<LinkPreview, SnScrappedLink?>
|
||||
with LinkPreviewRef {
|
||||
_LinkPreviewProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get url => (origin as LinkPreviewProvider).url;
|
||||
}
|
||||
|
||||
// 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
|
@ -102,6 +102,7 @@ Future<ThemeData> createAppTheme(
|
||||
),
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
behavior: useM3 ? SnackBarBehavior.floating : SnackBarBehavior.fixed,
|
||||
width: 480,
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
centerTitle: true,
|
||||
|
@ -8,13 +8,20 @@ class AppRouter extends RootStackRouter {
|
||||
|
||||
@override
|
||||
List<AutoRoute> get routes => [
|
||||
AutoRoute(page: PostComposeRoute.page, path: '/posts/compose'),
|
||||
AutoRoute(page: PostEditRoute.page, path: '/posts/:id/edit'),
|
||||
AutoRoute(page: CallRoute.page, path: '/chat/:id/call'),
|
||||
AutoRoute(page: EventCalanderRoute.page, path: '/account/:name/calendar'),
|
||||
AutoRoute(path: '/', page: AppWrapper.page, children: _appRoutes),
|
||||
];
|
||||
|
||||
List<AutoRoute> get _appRoutes => [
|
||||
// Standalone routes without bottom navigation
|
||||
AutoRoute(page: PostComposeRoute.page, path: 'posts/compose'),
|
||||
AutoRoute(page: PostEditRoute.page, path: 'posts/:id/edit'),
|
||||
AutoRoute(page: CallRoute.page, path: 'chat/:id/call'),
|
||||
AutoRoute(page: EventCalanderRoute.page, path: 'account/:name/calendar'),
|
||||
|
||||
// Main tabs with bottom navigation and shell routes for desktop layout
|
||||
AutoRoute(
|
||||
page: TabsRoute.page,
|
||||
path: '/',
|
||||
path: '',
|
||||
children: [
|
||||
AutoRoute(
|
||||
page: ExploreShellRoute.page,
|
||||
@ -58,7 +65,7 @@ class AppRouter extends RootStackRouter {
|
||||
),
|
||||
AutoRoute(
|
||||
page: CreatorHubShellRoute.page,
|
||||
path: '/creators',
|
||||
path: 'creators',
|
||||
children: [
|
||||
AutoRoute(page: CreatorHubRoute.page, path: ''),
|
||||
AutoRoute(page: CreatorPostListRoute.page, path: ':name/posts'),
|
||||
@ -81,11 +88,11 @@ class AppRouter extends RootStackRouter {
|
||||
AutoRoute(page: EditPublisherRoute.page, path: ':name/edit'),
|
||||
],
|
||||
),
|
||||
AutoRoute(page: LoginRoute.page, path: '/auth/login'),
|
||||
AutoRoute(page: CreateAccountRoute.page, path: '/auth/create-account'),
|
||||
AutoRoute(page: SettingsRoute.page, path: '/settings'),
|
||||
AutoRoute(page: NewRealmRoute.page, path: '/realms/new'),
|
||||
AutoRoute(page: RealmDetailRoute.page, path: '/realms/:slug'),
|
||||
AutoRoute(page: EditRealmRoute.page, path: '/realms/:slug/edit'),
|
||||
AutoRoute(page: LoginRoute.page, path: 'auth/login'),
|
||||
AutoRoute(page: CreateAccountRoute.page, path: 'auth/create-account'),
|
||||
AutoRoute(page: SettingsRoute.page, path: 'settings'),
|
||||
AutoRoute(page: NewRealmRoute.page, path: 'realms/new'),
|
||||
AutoRoute(page: RealmDetailRoute.page, path: 'realms/:slug'),
|
||||
AutoRoute(page: EditRealmRoute.page, path: 'realms/:slug/edit'),
|
||||
];
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -61,7 +61,7 @@ class EventCalanderScreen extends HookConsumerWidget {
|
||||
child: Column(
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.all(16),
|
||||
margin: EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Use the reusable EventCalendarWidget
|
||||
@ -77,7 +77,6 @@ class EventCalanderScreen extends HookConsumerWidget {
|
||||
),
|
||||
|
||||
// Add the fortune graph widget
|
||||
const Divider(height: 1),
|
||||
FortuneGraphWidget(
|
||||
events: events,
|
||||
constrainWidth: true,
|
||||
|
@ -641,7 +641,7 @@ class LevelingScreen extends HookConsumerWidget {
|
||||
ref.invalidate(accountStellarSubscriptionProvider);
|
||||
ref.read(userInfoProvider.notifier).fetchUser();
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'membershipPurchaseSuccess'.tr());
|
||||
showSnackBar('membershipPurchaseSuccess'.tr());
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -72,7 +72,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/accounts/me');
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'accountDeletionSent'.tr());
|
||||
showSnackBar('accountDeletionSent'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
@ -100,7 +100,7 @@ class AccountSettingsScreen extends HookConsumerWidget {
|
||||
data: {'account': userInfo.value!.name, 'captcha_token': captchaTk},
|
||||
);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'accountPasswordChangeSent'.tr());
|
||||
showSnackBar('accountPasswordChangeSent'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
|
@ -205,7 +205,7 @@ class AuthFactorNewSheet extends HookConsumerWidget {
|
||||
builder: (context) => AuthFactorNewAdditonalSheet(factor: factor),
|
||||
).then((_) {
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'contactMethodVerificationNeeded'.tr());
|
||||
showSnackBar('contactMethodVerificationNeeded'.tr());
|
||||
}
|
||||
if (context.mounted) Navigator.pop(context, true);
|
||||
});
|
||||
|
@ -181,7 +181,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'accountConnectionAddSuccess'.tr());
|
||||
showSnackBar('accountConnectionAddSuccess'.tr());
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
} catch (err) {
|
||||
@ -208,7 +208,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
||||
if (context.mounted) Navigator.pop(context, true);
|
||||
break;
|
||||
default:
|
||||
showSnackBar(context, 'accountConnectionAddError'.tr());
|
||||
showSnackBar('accountConnectionAddError'.tr());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ class ContactMethodSheet extends HookConsumerWidget {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post('/accounts/me/contacts/${contact.id}/verify');
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'contactMethodVerificationSent'.tr());
|
||||
showSnackBar('contactMethodVerificationSent'.tr());
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
@ -152,7 +152,7 @@ class ContactMethodNewSheet extends HookConsumerWidget {
|
||||
|
||||
Future<void> addContactMethod() async {
|
||||
if (contentController.text.isEmpty) {
|
||||
showSnackBar(context, 'contactMethodContentEmpty'.tr());
|
||||
showSnackBar('contactMethodContentEmpty'.tr());
|
||||
return;
|
||||
}
|
||||
|
||||
@ -164,7 +164,7 @@ class ContactMethodNewSheet extends HookConsumerWidget {
|
||||
data: {'type': contactType.value, 'content': contentController.text},
|
||||
);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'contactMethodVerificationNeeded'.tr());
|
||||
showSnackBar('contactMethodVerificationNeeded'.tr());
|
||||
Navigator.pop(context, true);
|
||||
}
|
||||
} catch (err) {
|
||||
|
@ -242,12 +242,10 @@ class RelationshipScreen extends HookConsumerWidget {
|
||||
if (!context.mounted) return;
|
||||
if (isAccept) {
|
||||
showSnackBar(
|
||||
context,
|
||||
'friendRequestAccepted'.tr(args: ['@${relationship.account.name}']),
|
||||
);
|
||||
} else {
|
||||
showSnackBar(
|
||||
context,
|
||||
'friendRequestDeclined'.tr(args: ['@${relationship.account.name}']),
|
||||
);
|
||||
}
|
||||
|
@ -427,7 +427,7 @@ class _LoginPickerScreen extends HookConsumerWidget {
|
||||
onPickFactor(factors!.where((x) => x == factorPicked.value).first);
|
||||
onNext();
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, err.response!.data.toString());
|
||||
showSnackBar(err.response!.data.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/services/udid.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/app_scaffold.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@ -204,12 +205,7 @@ class _OidcScreenState extends ConsumerState<OidcScreen> {
|
||||
onPressed: () {
|
||||
if (currentUrl != null) {
|
||||
Clipboard.setData(ClipboardData(text: currentUrl!));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('copyToClipboard').tr(),
|
||||
duration: const Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
showSnackBar('copyToClipboard');
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -917,8 +917,10 @@ class _ChatInput extends HookConsumerWidget {
|
||||
final isMobile = !kIsWeb && (Platform.isAndroid || Platform.isIOS);
|
||||
|
||||
void send() {
|
||||
inputFocusNode.requestFocus();
|
||||
onSend.call();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
inputFocusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> handlePaste() async {
|
||||
|
@ -49,7 +49,6 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
ref.invalidate(chatroomIdentityProvider(id));
|
||||
if (context.mounted) {
|
||||
showSnackBar(
|
||||
context,
|
||||
'chatNotifyLevelUpdated'.tr(args: [kNotifyLevelText[level].tr()]),
|
||||
);
|
||||
}
|
||||
@ -140,7 +139,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
setChatBreak(now);
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'chatBreakCleared'.tr());
|
||||
showSnackBar('chatBreakCleared'.tr());
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -152,7 +151,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
setChatBreak(now.add(const Duration(minutes: 5)));
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'chatBreakSet'.tr(args: ['5m']));
|
||||
showSnackBar('chatBreakSet'.tr(args: ['5m']));
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -164,7 +163,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
setChatBreak(now.add(const Duration(minutes: 10)));
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'chatBreakSet'.tr(args: ['10m']));
|
||||
showSnackBar('chatBreakSet'.tr(args: ['10m']));
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -176,7 +175,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
setChatBreak(now.add(const Duration(minutes: 15)));
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'chatBreakSet'.tr(args: ['15m']));
|
||||
showSnackBar('chatBreakSet'.tr(args: ['15m']));
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -188,7 +187,7 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
setChatBreak(now.add(const Duration(minutes: 30)));
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'chatBreakSet'.tr(args: ['30m']));
|
||||
showSnackBar('chatBreakSet'.tr(args: ['30m']));
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -208,7 +207,6 @@ class ChatDetailScreen extends HookConsumerWidget {
|
||||
Navigator.pop(context);
|
||||
if (context.mounted) {
|
||||
showSnackBar(
|
||||
context,
|
||||
'chatBreakSet'.tr(args: ['${minutes}m']),
|
||||
);
|
||||
}
|
||||
|
@ -186,7 +186,6 @@ class NotificationScreen extends HookConsumerWidget {
|
||||
final uri = Uri.tryParse(href);
|
||||
if (uri == null) {
|
||||
showSnackBar(
|
||||
context,
|
||||
'brokenLink'.tr(args: []),
|
||||
action: SnackBarAction(
|
||||
label: 'copyToClipboard'.tr(),
|
||||
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
@ -22,6 +23,23 @@ import 'package:island/widgets/post/draft_manager.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'compose.freezed.dart';
|
||||
part 'compose.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class PostComposeInitialState with _$PostComposeInitialState {
|
||||
const factory PostComposeInitialState({
|
||||
String? title,
|
||||
String? description,
|
||||
String? content,
|
||||
@Default([]) List<UniversalFile> attachments,
|
||||
int? visibility,
|
||||
}) = _PostComposeInitialState;
|
||||
|
||||
factory PostComposeInitialState.fromJson(Map<String, dynamic> json) =>
|
||||
_$PostComposeInitialStateFromJson(json);
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class PostEditScreen extends HookConsumerWidget {
|
||||
final String id;
|
||||
@ -54,16 +72,16 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
final SnPost? repliedPost;
|
||||
final SnPost? forwardedPost;
|
||||
final int? type;
|
||||
final PostComposeInitialState? initialState;
|
||||
const PostComposeScreen({
|
||||
super.key,
|
||||
this.originalPost,
|
||||
this.repliedPost,
|
||||
this.forwardedPost,
|
||||
@QueryParam('type') this.type,
|
||||
this.initialState,
|
||||
});
|
||||
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Determine the compose type: auto-detect from edited post or use query parameter
|
||||
@ -96,7 +114,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
useEffect(() {
|
||||
if (originalPost == null) {
|
||||
// Only auto-save for new posts, not edits
|
||||
state.startAutoSave(ref);
|
||||
state.startAutoSave(ref, postType: 0);
|
||||
}
|
||||
return () => state.stopAutoSave();
|
||||
}, [state]);
|
||||
@ -109,23 +127,40 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
return null;
|
||||
}, [publishers]);
|
||||
|
||||
// Load draft if available (only for new posts)
|
||||
// Load initial state if provided (for sharing functionality)
|
||||
useEffect(() {
|
||||
if (initialState != null) {
|
||||
state.titleController.text = initialState!.title ?? '';
|
||||
state.descriptionController.text = initialState!.description ?? '';
|
||||
state.contentController.text = initialState!.content ?? '';
|
||||
if (initialState!.visibility != null) {
|
||||
state.visibility.value = initialState!.visibility!;
|
||||
}
|
||||
if (initialState!.attachments.isNotEmpty) {
|
||||
state.attachments.value = List.from(initialState!.attachments);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}, [initialState]);
|
||||
|
||||
// Load draft if available (only for new posts without initial state)
|
||||
useEffect(() {
|
||||
if (originalPost == null &&
|
||||
effectiveForwardedPost == null &&
|
||||
effectiveRepliedPost == null) {
|
||||
effectiveRepliedPost == null &&
|
||||
initialState == null) {
|
||||
// Try to load the most recent draft
|
||||
final drafts = ref.read(composeStorageNotifierProvider);
|
||||
if (drafts.isNotEmpty) {
|
||||
final mostRecentDraft = drafts.values.reduce(
|
||||
(a, b) => a.lastModified.isAfter(b.lastModified) ? a : b,
|
||||
(a, b) => (a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0)) ? a : b,
|
||||
);
|
||||
|
||||
// Only load if the draft has meaningful content
|
||||
if (!mostRecentDraft.isEmpty) {
|
||||
state.titleController.text = mostRecentDraft.title;
|
||||
state.descriptionController.text = mostRecentDraft.description;
|
||||
state.contentController.text = mostRecentDraft.content;
|
||||
if (mostRecentDraft.content?.isNotEmpty == true || mostRecentDraft.title?.isNotEmpty == true) {
|
||||
state.titleController.text = mostRecentDraft.title ?? '';
|
||||
state.descriptionController.text = mostRecentDraft.description ?? '';
|
||||
state.contentController.text = mostRecentDraft.content ?? '';
|
||||
state.visibility.value = mostRecentDraft.visibility;
|
||||
}
|
||||
}
|
||||
@ -162,9 +197,10 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
Widget buildWideAttachmentGrid() {
|
||||
return GridView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
crossAxisCount: 2,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
@ -245,17 +281,16 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => DraftManagerSheet(
|
||||
isArticle: false,
|
||||
onDraftSelected: (draftId) {
|
||||
final draft =
|
||||
ref.read(
|
||||
composeStorageNotifierProvider,
|
||||
)[draftId];
|
||||
if (draft != null) {
|
||||
state.titleController.text = draft.title;
|
||||
state.titleController.text = draft.title ?? '';
|
||||
state.descriptionController.text =
|
||||
draft.description;
|
||||
state.contentController.text = draft.content;
|
||||
draft.description ?? '';
|
||||
state.contentController.text = draft.content ?? '';
|
||||
state.visibility.value = draft.visibility;
|
||||
}
|
||||
},
|
||||
@ -320,7 +355,7 @@ class PostComposeScreen extends HookConsumerWidget {
|
||||
// Main content area
|
||||
Expanded(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 480),
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
166
lib/screens/posts/compose.freezed.dart
Normal file
166
lib/screens/posts/compose.freezed.dart
Normal file
@ -0,0 +1,166 @@
|
||||
// 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 'compose.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$PostComposeInitialState {
|
||||
|
||||
String? get title; String? get description; String? get content; List<UniversalFile> get attachments; int? get visibility;
|
||||
/// Create a copy of PostComposeInitialState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$PostComposeInitialStateCopyWith<PostComposeInitialState> get copyWith => _$PostComposeInitialStateCopyWithImpl<PostComposeInitialState>(this as PostComposeInitialState, _$identity);
|
||||
|
||||
/// Serializes this PostComposeInitialState to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other.attachments, attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(attachments),visibility);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $PostComposeInitialStateCopyWith<$Res> {
|
||||
factory $PostComposeInitialStateCopyWith(PostComposeInitialState value, $Res Function(PostComposeInitialState) _then) = _$PostComposeInitialStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$PostComposeInitialStateCopyWithImpl<$Res>
|
||||
implements $PostComposeInitialStateCopyWith<$Res> {
|
||||
_$PostComposeInitialStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final PostComposeInitialState _self;
|
||||
final $Res Function(PostComposeInitialState) _then;
|
||||
|
||||
/// Create a copy of PostComposeInitialState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
title: freezed == 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?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,attachments: null == attachments ? _self.attachments : attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<UniversalFile>,visibility: freezed == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _PostComposeInitialState implements PostComposeInitialState {
|
||||
const _PostComposeInitialState({this.title, this.description, this.content, final List<UniversalFile> attachments = const [], this.visibility}): _attachments = attachments;
|
||||
factory _PostComposeInitialState.fromJson(Map<String, dynamic> json) => _$PostComposeInitialStateFromJson(json);
|
||||
|
||||
@override final String? title;
|
||||
@override final String? description;
|
||||
@override final String? content;
|
||||
final List<UniversalFile> _attachments;
|
||||
@override@JsonKey() List<UniversalFile> get attachments {
|
||||
if (_attachments is EqualUnmodifiableListView) return _attachments;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_attachments);
|
||||
}
|
||||
|
||||
@override final int? visibility;
|
||||
|
||||
/// Create a copy of PostComposeInitialState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$PostComposeInitialStateCopyWith<_PostComposeInitialState> get copyWith => __$PostComposeInitialStateCopyWithImpl<_PostComposeInitialState>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$PostComposeInitialStateToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PostComposeInitialState&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other._attachments, _attachments)&&(identical(other.visibility, visibility) || other.visibility == visibility));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,title,description,content,const DeepCollectionEquality().hash(_attachments),visibility);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PostComposeInitialState(title: $title, description: $description, content: $content, attachments: $attachments, visibility: $visibility)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$PostComposeInitialStateCopyWith<$Res> implements $PostComposeInitialStateCopyWith<$Res> {
|
||||
factory _$PostComposeInitialStateCopyWith(_PostComposeInitialState value, $Res Function(_PostComposeInitialState) _then) = __$PostComposeInitialStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String? title, String? description, String? content, List<UniversalFile> attachments, int? visibility
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$PostComposeInitialStateCopyWithImpl<$Res>
|
||||
implements _$PostComposeInitialStateCopyWith<$Res> {
|
||||
__$PostComposeInitialStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _PostComposeInitialState _self;
|
||||
final $Res Function(_PostComposeInitialState) _then;
|
||||
|
||||
/// Create a copy of PostComposeInitialState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? title = freezed,Object? description = freezed,Object? content = freezed,Object? attachments = null,Object? visibility = freezed,}) {
|
||||
return _then(_PostComposeInitialState(
|
||||
title: freezed == 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?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,attachments: null == attachments ? _self._attachments : attachments // ignore: cast_nullable_to_non_nullable
|
||||
as List<UniversalFile>,visibility: freezed == visibility ? _self.visibility : visibility // ignore: cast_nullable_to_non_nullable
|
||||
as int?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
31
lib/screens/posts/compose.g.dart
Normal file
31
lib/screens/posts/compose.g.dart
Normal file
@ -0,0 +1,31 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'compose.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_PostComposeInitialState _$PostComposeInitialStateFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _PostComposeInitialState(
|
||||
title: json['title'] as String?,
|
||||
description: json['description'] as String?,
|
||||
content: json['content'] as String?,
|
||||
attachments:
|
||||
(json['attachments'] as List<dynamic>?)
|
||||
?.map((e) => UniversalFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
visibility: (json['visibility'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$PostComposeInitialStateToJson(
|
||||
_PostComposeInitialState instance,
|
||||
) => <String, dynamic>{
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'content': instance.content,
|
||||
'attachments': instance.attachments.map((e) => e.toJson()).toList(),
|
||||
'visibility': instance.visibility,
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -9,6 +9,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
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';
|
||||
@ -21,6 +22,7 @@ import 'package:island/widgets/post/compose_settings_sheet.dart';
|
||||
import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:island/widgets/post/publishers_modal.dart';
|
||||
import 'package:island/widgets/post/draft_manager.dart';
|
||||
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
@ -71,7 +73,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
if (originalPost == null) {
|
||||
// Only auto-save for new articles, not edits
|
||||
autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
_saveArticleDraft(ref, state);
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1);
|
||||
});
|
||||
}
|
||||
return () {
|
||||
@ -79,7 +81,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
state.stopAutoSave();
|
||||
// Save final draft before disposing
|
||||
if (originalPost == null) {
|
||||
_saveArticleDraft(ref, state);
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1);
|
||||
}
|
||||
ComposeLogic.dispose(state);
|
||||
autoSaveTimer?.cancel();
|
||||
@ -100,17 +102,22 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
useEffect(() {
|
||||
if (originalPost == null) {
|
||||
// Try to load the most recent article draft
|
||||
final drafts = ref.read(articleStorageNotifierProvider);
|
||||
final drafts = ref.read(composeStorageNotifierProvider);
|
||||
if (drafts.isNotEmpty) {
|
||||
final mostRecentDraft = drafts.values.reduce(
|
||||
(a, b) => a.lastModified.isAfter(b.lastModified) ? a : b,
|
||||
(a, b) =>
|
||||
(a.updatedAt ?? DateTime(0)).isAfter(b.updatedAt ?? DateTime(0))
|
||||
? a
|
||||
: b,
|
||||
);
|
||||
|
||||
// Only load if the draft has meaningful content
|
||||
if (!mostRecentDraft.isEmpty) {
|
||||
state.titleController.text = mostRecentDraft.title;
|
||||
state.descriptionController.text = mostRecentDraft.description;
|
||||
state.contentController.text = mostRecentDraft.content;
|
||||
if (mostRecentDraft.content?.isNotEmpty == true ||
|
||||
mostRecentDraft.title?.isNotEmpty == true) {
|
||||
state.titleController.text = mostRecentDraft.title ?? '';
|
||||
state.descriptionController.text =
|
||||
mostRecentDraft.description ?? '';
|
||||
state.contentController.text = mostRecentDraft.content ?? '';
|
||||
state.visibility.value = mostRecentDraft.visibility;
|
||||
}
|
||||
}
|
||||
@ -356,7 +363,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
return PopScope(
|
||||
onPopInvoked: (_) {
|
||||
if (originalPost == null) {
|
||||
_saveArticleDraft(ref, state);
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, state, postType: 1);
|
||||
}
|
||||
},
|
||||
child: AppScaffold(
|
||||
@ -383,17 +390,17 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
isScrollControlled: true,
|
||||
builder:
|
||||
(context) => DraftManagerSheet(
|
||||
isArticle: true,
|
||||
onDraftSelected: (draftId) {
|
||||
final draft =
|
||||
ref.read(
|
||||
articleStorageNotifierProvider,
|
||||
composeStorageNotifierProvider,
|
||||
)[draftId];
|
||||
if (draft != null) {
|
||||
state.titleController.text = draft.title;
|
||||
state.titleController.text = draft.title ?? '';
|
||||
state.descriptionController.text =
|
||||
draft.description;
|
||||
state.contentController.text = draft.content;
|
||||
draft.description ?? '';
|
||||
state.contentController.text =
|
||||
draft.content ?? '';
|
||||
state.visibility.value = draft.visibility;
|
||||
}
|
||||
},
|
||||
@ -404,7 +411,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.save),
|
||||
onPressed: () => _saveArticleDraft(ref, state),
|
||||
onPressed: () => ComposeLogic.saveDraft(ref, state, postType: 1),
|
||||
tooltip: 'saveDraft'.tr(),
|
||||
),
|
||||
IconButton(
|
||||
@ -524,7 +531,7 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
if (isPaste && isModifierPressed) {
|
||||
ComposeLogic.handlePaste(state);
|
||||
} else if (isSave && isModifierPressed) {
|
||||
_saveArticleDraft(ref, state);
|
||||
ComposeLogic.saveDraft(ref, state, postType: 1);
|
||||
} else if (isSubmit && isModifierPressed && !state.submitting.value) {
|
||||
ComposeLogic.performAction(
|
||||
ref,
|
||||
@ -537,23 +544,5 @@ class ArticleComposeScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
// Helper method to save article draft
|
||||
Future<void> _saveArticleDraft(WidgetRef ref, ComposeState state) async {
|
||||
try {
|
||||
final draft = ArticleDraftModel(
|
||||
id: state.draftId,
|
||||
title: state.titleController.text,
|
||||
description: state.descriptionController.text,
|
||||
content: state.contentController.text,
|
||||
visibility: state.visibility.value,
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
|
||||
await ref.read(articleStorageNotifierProvider.notifier).saveDraft(draft);
|
||||
} catch (e) {
|
||||
log('[ArticleCompose] Failed to save draft, error: $e');
|
||||
// Silently fail for auto-save to avoid disrupting user experience
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setCustomFonts(null);
|
||||
showSnackBar(context, 'settingsApplied'.tr());
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
},
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
@ -122,7 +122,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
ref
|
||||
.read(appSettingsNotifierProvider.notifier)
|
||||
.setCustomFonts(value.isEmpty ? null : value);
|
||||
showSnackBar(context, 'settingsApplied'.tr());
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -215,7 +215,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
prefs.setBool(kAppBackgroundStoreKey, true);
|
||||
ref.invalidate(backgroundImageFileProvider);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'settingsApplied'.tr());
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
}
|
||||
},
|
||||
),
|
||||
@ -243,7 +243,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
prefs.remove(kAppBackgroundStoreKey);
|
||||
ref.invalidate(backgroundImageFileProvider);
|
||||
if (context.mounted) {
|
||||
showSnackBar(context, 'settingsApplied'.tr());
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -290,7 +290,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
.setAppColorScheme(color.value);
|
||||
if (context.mounted) {
|
||||
hideLoadingModal(context);
|
||||
showSnackBar(context, 'settingsApplied'.tr());
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -321,7 +321,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
kNetworkServerDefault,
|
||||
);
|
||||
ref.invalidate(serverUrlProvider);
|
||||
showSnackBar(context, 'settingsApplied'.tr());
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
},
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
@ -333,7 +333,7 @@ class SettingsScreen extends HookConsumerWidget {
|
||||
if (value.isNotEmpty) {
|
||||
prefs.setString(kNetworkServerStoreKey, value);
|
||||
ref.invalidate(serverUrlProvider);
|
||||
showSnackBar(context, 'settingsApplied'.tr());
|
||||
showSnackBar('settingsApplied'.tr());
|
||||
}
|
||||
},
|
||||
),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:ui';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@ -6,8 +7,40 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/notification.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/navigation/conditional_bottom_nav.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
final currentRouteProvider = StateProvider<String?>((ref) => null);
|
||||
|
||||
class TabNavigationObserver extends AutoRouterObserver {
|
||||
Function(String?) onChange;
|
||||
TabNavigationObserver({required this.onChange});
|
||||
|
||||
@override
|
||||
void didPush(Route route, Route? previousRoute) {
|
||||
log('pushed ${previousRoute?.settings.name} -> ${route.settings.name}');
|
||||
if (route is DialogRoute) return;
|
||||
final name = route.settings.name;
|
||||
if (name == null) return;
|
||||
if (name.contains('Shell')) return;
|
||||
Future(() {
|
||||
onChange(name);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void didPop(Route route, Route? previousRoute) {
|
||||
log('popped ${route.settings.name} -> ${previousRoute?.settings.name}');
|
||||
if (previousRoute is DialogRoute) return;
|
||||
final name = previousRoute?.settings.name;
|
||||
if (name == null) return;
|
||||
if (name.contains('Shell')) return;
|
||||
Future(() {
|
||||
onChange(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@RoutePage()
|
||||
class TabsScreen extends HookConsumerWidget {
|
||||
const TabsScreen({super.key});
|
||||
@ -41,10 +74,10 @@ class TabsScreen extends HookConsumerWidget {
|
||||
];
|
||||
|
||||
final routes = <PageRouteInfo>[
|
||||
ExploreRoute(),
|
||||
ChatListRoute(),
|
||||
ExploreShellRoute(),
|
||||
ChatShellRoute(),
|
||||
RealmListRoute(),
|
||||
AccountRoute(),
|
||||
AccountShellRoute(),
|
||||
];
|
||||
|
||||
return AutoTabsRouter.tabBar(
|
||||
@ -83,31 +116,33 @@ class TabsScreen extends HookConsumerWidget {
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: ClipRRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surface.withOpacity(0.8),
|
||||
),
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: NavigationBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
overlayColor: WidgetStatePropertyAll(
|
||||
Colors.transparent,
|
||||
child: ConditionalBottomNav(
|
||||
child: ClipRRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surface.withOpacity(0.8),
|
||||
),
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeTop: true,
|
||||
child: NavigationBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
overlayColor: const WidgetStatePropertyAll(
|
||||
Colors.transparent,
|
||||
),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
height: 56,
|
||||
labelBehavior:
|
||||
NavigationDestinationLabelBehavior.alwaysHide,
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
onDestinationSelected: tabsRouter.setActiveIndex,
|
||||
destinations: destinations,
|
||||
),
|
||||
surfaceTintColor: Colors.transparent,
|
||||
height: 56,
|
||||
labelBehavior:
|
||||
NavigationDestinationLabelBehavior.alwaysHide,
|
||||
selectedIndex: tabsRouter.activeIndex,
|
||||
onDestinationSelected: tabsRouter.setActiveIndex,
|
||||
destinations: destinations,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -144,7 +179,7 @@ class TabbedFabLocation extends FloatingActionButtonLocation {
|
||||
scaffoldGeometry.floatingActionButtonSize.height -
|
||||
scaffoldGeometry.bottomSheetSize.height -
|
||||
safeAreaPadding.bottom -
|
||||
(isWideScreen(context) ? 24 : 80) +
|
||||
(isWideScreen(context) ? 32 : 80) +
|
||||
16;
|
||||
|
||||
return Offset(fabX, fabY);
|
||||
|
@ -1,183 +1,16 @@
|
||||
import 'dart:convert';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:island/database/drift_db.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/database.dart';
|
||||
import 'package:island/services/file.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'compose_storage_db.g.dart';
|
||||
|
||||
class ComposeDraftModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String description;
|
||||
final String content;
|
||||
final List<UniversalFile> attachments;
|
||||
final int visibility;
|
||||
final DateTime lastModified;
|
||||
|
||||
ComposeDraftModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.content,
|
||||
required this.attachments,
|
||||
required this.visibility,
|
||||
required this.lastModified,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'content': content,
|
||||
'attachments': attachments.map((e) => e.toJson()).toList(),
|
||||
'visibility': visibility,
|
||||
'lastModified': lastModified.toIso8601String(),
|
||||
};
|
||||
|
||||
factory ComposeDraftModel.fromJson(Map<String, dynamic> json) => ComposeDraftModel(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String? ?? '',
|
||||
description: json['description'] as String? ?? '',
|
||||
content: json['content'] as String? ?? '',
|
||||
attachments: (json['attachments'] as List? ?? [])
|
||||
.map((e) => UniversalFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
visibility: json['visibility'] as int? ?? 0,
|
||||
lastModified: DateTime.parse(json['lastModified'] as String),
|
||||
);
|
||||
|
||||
factory ComposeDraftModel.fromDbRow(ComposeDraft row) => ComposeDraftModel(
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
content: row.content,
|
||||
attachments: (jsonDecode(row.attachmentIds) as List)
|
||||
.map((e) => UniversalFile.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
visibility: row.visibility,
|
||||
lastModified: row.lastModified,
|
||||
);
|
||||
|
||||
ComposeDraftsCompanion toDbCompanion() => ComposeDraftsCompanion(
|
||||
id: Value(id),
|
||||
title: Value(title),
|
||||
description: Value(description),
|
||||
content: Value(content),
|
||||
attachmentIds: Value(jsonEncode(attachments.map((e) => e.toJson()).toList())),
|
||||
visibility: Value(visibility),
|
||||
lastModified: Value(lastModified),
|
||||
);
|
||||
|
||||
ComposeDraftModel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
String? content,
|
||||
List<UniversalFile>? attachments,
|
||||
int? visibility,
|
||||
DateTime? lastModified,
|
||||
}) {
|
||||
return ComposeDraftModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
content: content ?? this.content,
|
||||
attachments: attachments ?? this.attachments,
|
||||
visibility: visibility ?? this.visibility,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isEmpty =>
|
||||
title.isEmpty &&
|
||||
description.isEmpty &&
|
||||
content.isEmpty &&
|
||||
attachments.isEmpty;
|
||||
}
|
||||
|
||||
class ArticleDraftModel {
|
||||
final String id;
|
||||
final String title;
|
||||
final String description;
|
||||
final String content;
|
||||
final int visibility;
|
||||
final DateTime lastModified;
|
||||
|
||||
ArticleDraftModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.content,
|
||||
required this.visibility,
|
||||
required this.lastModified,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'content': content,
|
||||
'visibility': visibility,
|
||||
'lastModified': lastModified.toIso8601String(),
|
||||
};
|
||||
|
||||
factory ArticleDraftModel.fromJson(Map<String, dynamic> json) => ArticleDraftModel(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String? ?? '',
|
||||
description: json['description'] as String? ?? '',
|
||||
content: json['content'] as String? ?? '',
|
||||
visibility: json['visibility'] as int? ?? 0,
|
||||
lastModified: DateTime.parse(json['lastModified'] as String),
|
||||
);
|
||||
|
||||
factory ArticleDraftModel.fromDbRow(ArticleDraft row) => ArticleDraftModel(
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
description: row.description,
|
||||
content: row.content,
|
||||
visibility: row.visibility,
|
||||
lastModified: row.lastModified,
|
||||
);
|
||||
|
||||
ArticleDraftsCompanion toDbCompanion() => ArticleDraftsCompanion(
|
||||
id: Value(id),
|
||||
title: Value(title),
|
||||
description: Value(description),
|
||||
content: Value(content),
|
||||
visibility: Value(visibility),
|
||||
lastModified: Value(lastModified),
|
||||
);
|
||||
|
||||
ArticleDraftModel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
String? content,
|
||||
int? visibility,
|
||||
DateTime? lastModified,
|
||||
}) {
|
||||
return ArticleDraftModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
content: content ?? this.content,
|
||||
visibility: visibility ?? this.visibility,
|
||||
lastModified: lastModified ?? this.lastModified,
|
||||
);
|
||||
}
|
||||
|
||||
bool get isEmpty => title.isEmpty && description.isEmpty && content.isEmpty;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ComposeStorageNotifier extends _$ComposeStorageNotifier {
|
||||
@override
|
||||
Map<String, ComposeDraftModel> build() {
|
||||
Map<String, SnPost> build() {
|
||||
_loadDrafts();
|
||||
return {};
|
||||
}
|
||||
@ -185,10 +18,9 @@ class ComposeStorageNotifier extends _$ComposeStorageNotifier {
|
||||
void _loadDrafts() async {
|
||||
try {
|
||||
final database = ref.read(databaseProvider);
|
||||
final dbDrafts = await database.getAllComposeDrafts();
|
||||
final drafts = <String, ComposeDraftModel>{};
|
||||
for (final dbDraft in dbDrafts) {
|
||||
final draft = ComposeDraftModel.fromDbRow(dbDraft);
|
||||
final dbDrafts = await database.getAllPostDrafts();
|
||||
final drafts = <String, SnPost>{};
|
||||
for (final draft in dbDrafts) {
|
||||
drafts[draft.id] = draft;
|
||||
}
|
||||
state = drafts;
|
||||
@ -198,52 +30,22 @@ class ComposeStorageNotifier extends _$ComposeStorageNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveDraft(ComposeDraftModel draft) async {
|
||||
if (draft.isEmpty) {
|
||||
await deleteDraft(draft.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Upload all attachments that are not yet uploaded
|
||||
final uploadedAttachments = <UniversalFile>[];
|
||||
final serverUrl = ref.read(serverUrlProvider);
|
||||
final token = ref.read(tokenProvider);
|
||||
|
||||
for (final attachment in draft.attachments) {
|
||||
if (!attachment.isOnCloud) {
|
||||
try {
|
||||
final completer = putMediaToCloud(
|
||||
fileData: attachment,
|
||||
atk: token?.token ?? '',
|
||||
baseUrl: serverUrl,
|
||||
);
|
||||
final uploadedFile = await completer.future;
|
||||
if (uploadedFile != null) {
|
||||
uploadedAttachments.add(UniversalFile.fromAttachment(uploadedFile));
|
||||
} else {
|
||||
uploadedAttachments.add(attachment);
|
||||
}
|
||||
} catch (e) {
|
||||
// If upload fails, keep the original file
|
||||
uploadedAttachments.add(attachment);
|
||||
}
|
||||
} else {
|
||||
uploadedAttachments.add(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
final updatedDraft = draft.copyWith(
|
||||
attachments: uploadedAttachments,
|
||||
lastModified: DateTime.now(),
|
||||
);
|
||||
Future<void> saveDraft(SnPost draft) async {
|
||||
final updatedDraft = draft.copyWith(updatedAt: DateTime.now());
|
||||
state = {...state, updatedDraft.id: updatedDraft};
|
||||
|
||||
|
||||
try {
|
||||
final database = ref.read(databaseProvider);
|
||||
await database.saveComposeDraft(updatedDraft.toDbCompanion());
|
||||
await database.addPostDraft(
|
||||
PostDraftsCompanion(
|
||||
id: Value(updatedDraft.id),
|
||||
post: Value(jsonEncode(updatedDraft.toJson())),
|
||||
lastModified: Value(updatedDraft.updatedAt ?? DateTime.now()),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// Revert state on error
|
||||
final newState = Map<String, ComposeDraftModel>.from(state);
|
||||
final newState = Map<String, SnPost>.from(state);
|
||||
newState.remove(updatedDraft.id);
|
||||
state = newState;
|
||||
rethrow;
|
||||
@ -252,13 +54,13 @@ class ComposeStorageNotifier extends _$ComposeStorageNotifier {
|
||||
|
||||
Future<void> deleteDraft(String id) async {
|
||||
final oldDraft = state[id];
|
||||
final newState = Map<String, ComposeDraftModel>.from(state);
|
||||
final newState = Map<String, SnPost>.from(state);
|
||||
newState.remove(id);
|
||||
state = newState;
|
||||
|
||||
|
||||
try {
|
||||
final database = ref.read(databaseProvider);
|
||||
await database.deleteComposeDraft(id);
|
||||
await database.deletePostDraft(id);
|
||||
} catch (e) {
|
||||
// Revert state on error
|
||||
if (oldDraft != null) {
|
||||
@ -268,22 +70,22 @@ class ComposeStorageNotifier extends _$ComposeStorageNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
ComposeDraftModel? getDraft(String id) {
|
||||
SnPost? getDraft(String id) {
|
||||
return state[id];
|
||||
}
|
||||
|
||||
List<ComposeDraftModel> getAllDrafts() {
|
||||
List<SnPost> getAllDrafts() {
|
||||
final drafts = state.values.toList();
|
||||
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||
drafts.sort((a, b) => b.updatedAt!.compareTo(a.updatedAt!));
|
||||
return drafts;
|
||||
}
|
||||
|
||||
Future<void> clearAllDrafts() async {
|
||||
state = {};
|
||||
|
||||
|
||||
try {
|
||||
final database = ref.read(databaseProvider);
|
||||
await database.clearAllComposeDrafts();
|
||||
await database.clearAllPostDrafts();
|
||||
} catch (e) {
|
||||
// If clearing fails, we might want to reload from database
|
||||
_loadDrafts();
|
||||
@ -291,90 +93,3 @@ class ComposeStorageNotifier extends _$ComposeStorageNotifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class ArticleStorageNotifier extends _$ArticleStorageNotifier {
|
||||
@override
|
||||
Map<String, ArticleDraftModel> build() {
|
||||
_loadDrafts();
|
||||
return {};
|
||||
}
|
||||
|
||||
void _loadDrafts() async {
|
||||
try {
|
||||
final database = ref.read(databaseProvider);
|
||||
final dbDrafts = await database.getAllArticleDrafts();
|
||||
final drafts = <String, ArticleDraftModel>{};
|
||||
for (final dbDraft in dbDrafts) {
|
||||
final draft = ArticleDraftModel.fromDbRow(dbDraft);
|
||||
drafts[draft.id] = draft;
|
||||
}
|
||||
state = drafts;
|
||||
} catch (e) {
|
||||
// If there's an error loading drafts, start with empty state
|
||||
state = {};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveDraft(ArticleDraftModel draft) async {
|
||||
if (draft.isEmpty) {
|
||||
await deleteDraft(draft.id);
|
||||
return;
|
||||
}
|
||||
|
||||
final updatedDraft = draft.copyWith(lastModified: DateTime.now());
|
||||
state = {...state, updatedDraft.id: updatedDraft};
|
||||
|
||||
try {
|
||||
final database = ref.read(databaseProvider);
|
||||
await database.saveArticleDraft(updatedDraft.toDbCompanion());
|
||||
} catch (e) {
|
||||
// Revert state on error
|
||||
final newState = Map<String, ArticleDraftModel>.from(state);
|
||||
newState.remove(updatedDraft.id);
|
||||
state = newState;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteDraft(String id) async {
|
||||
final oldDraft = state[id];
|
||||
final newState = Map<String, ArticleDraftModel>.from(state);
|
||||
newState.remove(id);
|
||||
state = newState;
|
||||
|
||||
try {
|
||||
final database = ref.read(databaseProvider);
|
||||
await database.deleteArticleDraft(id);
|
||||
} catch (e) {
|
||||
// Revert state on error
|
||||
if (oldDraft != null) {
|
||||
state = {...state, id: oldDraft};
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
ArticleDraftModel? getDraft(String id) {
|
||||
return state[id];
|
||||
}
|
||||
|
||||
List<ArticleDraftModel> getAllDrafts() {
|
||||
final drafts = state.values.toList();
|
||||
drafts.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||
return drafts;
|
||||
}
|
||||
|
||||
Future<void> clearAllDrafts() async {
|
||||
state = {};
|
||||
|
||||
try {
|
||||
final database = ref.read(databaseProvider);
|
||||
await database.clearAllArticleDrafts();
|
||||
} catch (e) {
|
||||
// If clearing fails, we might want to reload from database
|
||||
_loadDrafts();
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,13 +7,13 @@ part of 'compose_storage_db.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$composeStorageNotifierHash() =>
|
||||
r'fcdb006dca44d30916a20804922e93d0caad49ca';
|
||||
r'4ab4dce85d0a961f096dc3b11505f8f0964dee9d';
|
||||
|
||||
/// See also [ComposeStorageNotifier].
|
||||
@ProviderFor(ComposeStorageNotifier)
|
||||
final composeStorageNotifierProvider = AutoDisposeNotifierProvider<
|
||||
ComposeStorageNotifier,
|
||||
Map<String, ComposeDraftModel>
|
||||
Map<String, SnPost>
|
||||
>.internal(
|
||||
ComposeStorageNotifier.new,
|
||||
name: r'composeStorageNotifierProvider',
|
||||
@ -25,28 +25,6 @@ final composeStorageNotifierProvider = AutoDisposeNotifierProvider<
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ComposeStorageNotifier =
|
||||
AutoDisposeNotifier<Map<String, ComposeDraftModel>>;
|
||||
String _$articleStorageNotifierHash() =>
|
||||
r'21ee0f8ee87528bebf8f5f4b0b2892cd8058e230';
|
||||
|
||||
/// See also [ArticleStorageNotifier].
|
||||
@ProviderFor(ArticleStorageNotifier)
|
||||
final articleStorageNotifierProvider = AutoDisposeNotifierProvider<
|
||||
ArticleStorageNotifier,
|
||||
Map<String, ArticleDraftModel>
|
||||
>.internal(
|
||||
ArticleStorageNotifier.new,
|
||||
name: r'articleStorageNotifierProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$articleStorageNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$ArticleStorageNotifier =
|
||||
AutoDisposeNotifier<Map<String, ArticleDraftModel>>;
|
||||
typedef _$ComposeStorageNotifier = AutoDisposeNotifier<Map<String, SnPost>>;
|
||||
// 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
|
||||
|
@ -1,9 +1,57 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/models/user.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/app_notification.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
StreamSubscription<WebSocketPacket> setupNotificationListener(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
) {
|
||||
final ws = ref.watch(websocketProvider);
|
||||
return ws.dataStream.listen((pkt) {
|
||||
if (pkt.type == "notifications.new") {
|
||||
final notification = SnNotification.fromJson(pkt.data!);
|
||||
showTopSnackBar(
|
||||
globalOverlay.currentState!,
|
||||
NotificationCard(notification: notification),
|
||||
onTap: () {
|
||||
if (notification.meta['action_uri'] != null) {
|
||||
var uri = notification.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
appRouter.pushPath(notification.meta['action_uri']);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismissed: () {},
|
||||
dismissType: DismissType.onSwipe,
|
||||
displayDuration: const Duration(seconds: 5),
|
||||
snackBarPosition: SnackBarPosition.top,
|
||||
padding: EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 16,
|
||||
// ignore: use_build_context_synchronously
|
||||
top: MediaQuery.of(context).padding.top + 24,
|
||||
bottom: 16,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> subscribePushNotification(Dio apiClient) async {
|
||||
await FirebaseMessaging.instance.requestPermission(
|
||||
|
123
lib/services/sharing_intent.dart
Normal file
123
lib/services/sharing_intent.dart
Normal file
@ -0,0 +1,123 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:island/widgets/share/share_sheet.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
class SharingIntentService {
|
||||
static final SharingIntentService _instance =
|
||||
SharingIntentService._internal();
|
||||
factory SharingIntentService() => _instance;
|
||||
SharingIntentService._internal();
|
||||
|
||||
StreamSubscription<List<SharedMediaFile>>? _intentSub;
|
||||
BuildContext? _context;
|
||||
|
||||
/// Initialize the sharing intent service
|
||||
void initialize(BuildContext context) {
|
||||
if (kIsWeb || !(Platform.isIOS || Platform.isAndroid)) return;
|
||||
debugPrint("SharingIntentService: Initializing with context");
|
||||
_context = context;
|
||||
_setupSharingListeners();
|
||||
}
|
||||
|
||||
/// Setup listeners for sharing intents
|
||||
void _setupSharingListeners() {
|
||||
debugPrint("SharingIntentService: Setting up sharing listeners");
|
||||
|
||||
// Listen to media sharing coming from outside the app while the app is in memory
|
||||
_intentSub = ReceiveSharingIntent.instance.getMediaStream().listen(
|
||||
(List<SharedMediaFile> value) {
|
||||
debugPrint(
|
||||
"SharingIntentService: Media stream received ${value.length} files",
|
||||
);
|
||||
if (value.isNotEmpty) {
|
||||
_handleSharedContent(value);
|
||||
}
|
||||
},
|
||||
onError: (err) {
|
||||
debugPrint("SharingIntentService: Stream error: $err");
|
||||
},
|
||||
);
|
||||
|
||||
// Get the media sharing coming from outside the app while the app is closed
|
||||
ReceiveSharingIntent.instance.getInitialMedia().then((
|
||||
List<SharedMediaFile> value,
|
||||
) {
|
||||
debugPrint(
|
||||
"SharingIntentService: Initial media received ${value.length} files",
|
||||
);
|
||||
if (value.isNotEmpty) {
|
||||
_handleSharedContent(value);
|
||||
// Tell the library that we are done processing the intent
|
||||
ReceiveSharingIntent.instance.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Handle shared media files
|
||||
void _handleSharedContent(List<SharedMediaFile> sharedFiles) {
|
||||
if (_context == null) {
|
||||
debugPrint(
|
||||
"SharingIntentService: Context is null, cannot handle shared content",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
"SharingIntentService: Received ${sharedFiles.length} shared files",
|
||||
);
|
||||
for (final file in sharedFiles) {
|
||||
debugPrint(
|
||||
"SharingIntentService: File path: ${file.path}, type: ${file.type}",
|
||||
);
|
||||
}
|
||||
|
||||
// Convert SharedMediaFile to XFile for files
|
||||
final List<XFile> files =
|
||||
sharedFiles
|
||||
.where(
|
||||
(file) =>
|
||||
file.type == SharedMediaType.file ||
|
||||
file.type == SharedMediaType.video ||
|
||||
file.type == SharedMediaType.image,
|
||||
)
|
||||
.map((file) => XFile(file.path, name: file.path.split('/').last))
|
||||
.toList();
|
||||
|
||||
// Extract links from shared content
|
||||
final List<String> links =
|
||||
sharedFiles
|
||||
.where((file) => file.type == SharedMediaType.url)
|
||||
.map((file) => file.path)
|
||||
.toList();
|
||||
|
||||
// Show ShareSheet with the shared files
|
||||
if (files.isNotEmpty) {
|
||||
showShareSheet(context: _context!, content: ShareContent.files(files));
|
||||
} else if (links.isNotEmpty) {
|
||||
showShareSheet(
|
||||
context: _context!,
|
||||
content: ShareContent.link(links.first),
|
||||
);
|
||||
} else {
|
||||
showShareSheet(
|
||||
context: _context!,
|
||||
content: ShareContent.text(
|
||||
sharedFiles
|
||||
.where((file) => file.type == SharedMediaType.text)
|
||||
.map((text) => text.message)
|
||||
.join('\n'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dispose of resources
|
||||
void dispose() {
|
||||
_intentSub?.cancel();
|
||||
_context = null;
|
||||
}
|
||||
}
|
@ -38,7 +38,7 @@ class RestorePurchaseSheet extends HookConsumerWidget {
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
showSnackBar(context, 'Purchase restored successfully!');
|
||||
showSnackBar('Purchase restored successfully!');
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
|
@ -1,31 +1,18 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:top_snackbar_flutter/top_snack_bar.dart';
|
||||
|
||||
export 'content/alert.native.dart'
|
||||
if (dart.library.html) 'content/alert.web.dart';
|
||||
|
||||
void showSnackBar(
|
||||
BuildContext context,
|
||||
String message, {
|
||||
SnackBarAction? action,
|
||||
}) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message),
|
||||
action: action,
|
||||
margin:
|
||||
isWideScreen(context)
|
||||
? null
|
||||
: EdgeInsets.fromLTRB(
|
||||
15.0,
|
||||
5.0,
|
||||
15.0,
|
||||
MediaQuery.of(context).padding.bottom + 28,
|
||||
),
|
||||
),
|
||||
void showSnackBar(String message, {SnackBarAction? action}) {
|
||||
showTopSnackBar(
|
||||
globalOverlay.currentState!,
|
||||
Card(child: Text(message).padding(horizontal: 20, vertical: 16)),
|
||||
snackBarPosition: SnackBarPosition.bottom,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,235 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/main.dart';
|
||||
import 'package:island/models/user.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
part 'app_notification.freezed.dart';
|
||||
part 'app_notification.g.dart';
|
||||
class NotificationCard extends HookConsumerWidget {
|
||||
final SnNotification notification;
|
||||
|
||||
class AppNotificationToast extends HookConsumerWidget {
|
||||
const AppNotificationToast({super.key});
|
||||
const NotificationCard({super.key, required this.notification});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final notifications = ref.watch(appNotificationsProvider);
|
||||
|
||||
// Create a global key for AnimatedList
|
||||
final listKey = useMemoized(() => GlobalKey<AnimatedListState>());
|
||||
|
||||
// Track visual notification count (including those being animated out)
|
||||
final visualCount = useState(notifications.length);
|
||||
|
||||
// Track notifications being removed to manage visual count
|
||||
final animatingOutIds = useState<Set<String>>({});
|
||||
|
||||
// Track previous notifications to detect changes
|
||||
final previousNotifications = usePrevious(notifications) ?? [];
|
||||
|
||||
// Handle notification changes
|
||||
useEffect(() {
|
||||
final currentIds = notifications.map((n) => n.data.id).toSet();
|
||||
final previousIds = previousNotifications.map((n) => n.data.id).toSet();
|
||||
|
||||
// Find new notifications (added)
|
||||
final newIds = currentIds.difference(previousIds);
|
||||
|
||||
// Update visual count for new notifications
|
||||
if (newIds.isNotEmpty) {
|
||||
visualCount.value += newIds.length;
|
||||
}
|
||||
|
||||
// Insert new notifications with animation
|
||||
for (final id in newIds) {
|
||||
final index = notifications.indexWhere((n) => n.data.id == id);
|
||||
if (index != -1 &&
|
||||
listKey.currentState != null &&
|
||||
index >= 0 &&
|
||||
index <= notifications.length) {
|
||||
try {
|
||||
listKey.currentState!.insertItem(
|
||||
index,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
);
|
||||
} catch (e) {
|
||||
// Log error but don't crash the app
|
||||
debugPrint('Error inserting notification: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [notifications]);
|
||||
|
||||
return Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 50,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: SizedBox(
|
||||
// Use visualCount instead of notifications.length for height calculation
|
||||
height: visualCount.value * 80,
|
||||
child: AnimatedList(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
key: listKey,
|
||||
initialItemCount: notifications.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
// Safely access notifications with bounds check
|
||||
if (index >= notifications.length) {
|
||||
return const SizedBox.shrink(); // Return empty widget if out of bounds
|
||||
}
|
||||
|
||||
final notification = notifications[index];
|
||||
final now = DateTime.now();
|
||||
final createdAt = notification.createdAt ?? now;
|
||||
final duration =
|
||||
notification.duration ?? const Duration(seconds: 5);
|
||||
final elapsedTime = now.difference(createdAt);
|
||||
final remainingTime = duration - elapsedTime;
|
||||
final progress =
|
||||
1.0 -
|
||||
(remainingTime.inMilliseconds / duration.inMilliseconds).clamp(
|
||||
0.0,
|
||||
1.0,
|
||||
); // Ensure progress is clamped
|
||||
|
||||
return SizeTransition(
|
||||
sizeFactor: animation.drive(
|
||||
CurveTween(curve: Curves.fastLinearToSlowEaseIn),
|
||||
),
|
||||
child: _NotificationCard(
|
||||
notification: notification,
|
||||
progress: progress.clamp(0.0, 1.0),
|
||||
onDismiss: () {
|
||||
// Find the current index before removal
|
||||
final currentIndex = notifications.indexWhere(
|
||||
(n) => n.data.id == notification.data.id,
|
||||
);
|
||||
|
||||
// Add to animating out set
|
||||
final notificationId = notification.data.id;
|
||||
if (!animatingOutIds.value.contains(notificationId)) {
|
||||
animatingOutIds.value = {
|
||||
...animatingOutIds.value,
|
||||
notificationId,
|
||||
};
|
||||
}
|
||||
|
||||
if (currentIndex != -1 &&
|
||||
listKey.currentState != null &&
|
||||
currentIndex >= 0 &&
|
||||
currentIndex < notifications.length) {
|
||||
try {
|
||||
// Remove the item with animation
|
||||
listKey.currentState!.removeItem(
|
||||
currentIndex,
|
||||
(context, animation) => SizeTransition(
|
||||
sizeFactor: animation.drive(
|
||||
CurveTween(curve: Curves.fastLinearToSlowEaseIn),
|
||||
),
|
||||
child: _NotificationCard(
|
||||
notification: notification,
|
||||
progress: progress.clamp(0.0, 1.0),
|
||||
onDismiss:
|
||||
() {}, // Empty because it's being removed
|
||||
),
|
||||
),
|
||||
duration: const Duration(milliseconds: 150),
|
||||
// When animation completes, update the visual count
|
||||
);
|
||||
|
||||
// Schedule decrementing the visual count after animation completes
|
||||
Future.delayed(const Duration(milliseconds: 150), () {
|
||||
if (animatingOutIds.value.contains(notificationId)) {
|
||||
visualCount.value =
|
||||
visualCount.value > 0 ? visualCount.value - 1 : 0;
|
||||
animatingOutIds.value =
|
||||
animatingOutIds.value
|
||||
.where((id) => id != notificationId)
|
||||
.toSet();
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// Log error but don't crash the app
|
||||
log('[Notification] Error removing notification: $e');
|
||||
// Still update visual count in case of error
|
||||
visualCount.value =
|
||||
visualCount.value > 0 ? visualCount.value - 1 : 0;
|
||||
animatingOutIds.value =
|
||||
animatingOutIds.value
|
||||
.where((id) => id != notificationId)
|
||||
.toSet();
|
||||
}
|
||||
}
|
||||
|
||||
// Actually remove from state
|
||||
ref
|
||||
.read(appNotificationsProvider.notifier)
|
||||
.removeNotification(notification);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NotificationCard extends HookConsumerWidget {
|
||||
final AppNotification notification;
|
||||
final double progress;
|
||||
final VoidCallback onDismiss;
|
||||
|
||||
const _NotificationCard({
|
||||
required this.notification,
|
||||
required this.progress,
|
||||
required this.onDismiss,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Use state to track the current progress for smooth animation
|
||||
final progressState = useState(progress);
|
||||
|
||||
// Use effect to update progress smoothly
|
||||
useEffect(() {
|
||||
if (progress < 1.0) {
|
||||
// Update progress every 16ms (roughly 60fps) for smooth animation
|
||||
final timer = Timer.periodic(const Duration(milliseconds: 16), (_) {
|
||||
final now = DateTime.now();
|
||||
final createdAt = notification.createdAt ?? now;
|
||||
final duration = notification.duration ?? const Duration(seconds: 5);
|
||||
final elapsedTime = now.difference(createdAt);
|
||||
final remainingTime = duration - elapsedTime;
|
||||
final newProgress = (1.0 -
|
||||
(remainingTime.inMilliseconds / duration.inMilliseconds))
|
||||
.clamp(0.0, 1.0);
|
||||
|
||||
progressState.value = newProgress;
|
||||
|
||||
// Auto-dismiss when complete
|
||||
if (newProgress >= 1.0) {
|
||||
onDismiss();
|
||||
}
|
||||
});
|
||||
|
||||
return timer.cancel;
|
||||
}
|
||||
return null;
|
||||
}, [notification.createdAt, notification.duration]);
|
||||
final icon = Symbols.info;
|
||||
|
||||
return Card(
|
||||
elevation: 4,
|
||||
@ -237,225 +20,52 @@ class _NotificationCard extends HookConsumerWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
onTap: () {
|
||||
if (notification.data.meta['action_uri'] != null) {
|
||||
var uri = notification.data.meta['action_uri'] as String;
|
||||
if (uri.startsWith('/')) {
|
||||
// In-app routes
|
||||
appRouter.pushPath(notification.data.meta['action_uri']);
|
||||
} else {
|
||||
// External URLs
|
||||
launchUrlString(uri);
|
||||
}
|
||||
onDismiss();
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Progress indicator
|
||||
if (progressState.value > 0 && progressState.value < 1.0)
|
||||
AnimatedBuilder(
|
||||
animation: progressState,
|
||||
builder: (context, _) {
|
||||
return LinearProgressIndicator(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
top: Radius.circular(16),
|
||||
),
|
||||
value: 1.0 - progressState.value,
|
||||
backgroundColor: Colors.transparent,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
minHeight: 3,
|
||||
stopIndicatorColor: Colors.transparent,
|
||||
stopIndicatorRadius: 0,
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (notification.data.meta['avatar'] != null)
|
||||
ProfilePictureWidget(
|
||||
fileId: notification.data.meta['avatar'],
|
||||
radius: 12,
|
||||
).padding(right: 12, top: 2)
|
||||
else if (notification.icon != null)
|
||||
Icon(
|
||||
notification.icon,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
).padding(right: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (notification.meta['pfp'] != null)
|
||||
ProfilePictureWidget(
|
||||
fileId: notification.meta['pfp'],
|
||||
radius: 12,
|
||||
).padding(right: 12, top: 2)
|
||||
else
|
||||
Icon(
|
||||
icon,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
).padding(right: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
notification.title,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (notification.content.isNotEmpty)
|
||||
Text(
|
||||
notification.data.title,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.bold),
|
||||
notification.content,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (notification.data.content.isNotEmpty)
|
||||
Text(
|
||||
notification.data.content,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (notification.data.subtitle.isNotEmpty)
|
||||
Text(
|
||||
notification.data.subtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (notification.subtitle.isNotEmpty)
|
||||
Text(
|
||||
notification.subtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close, size: 18),
|
||||
onPressed: onDismiss,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class AppNotification with _$AppNotification {
|
||||
const factory AppNotification({
|
||||
required SnNotification data,
|
||||
@JsonKey(ignore: true) IconData? icon,
|
||||
@JsonKey(ignore: true) Duration? duration,
|
||||
@Default(null) DateTime? createdAt,
|
||||
@Default(false) @JsonKey(ignore: true) bool isAnimatingOut,
|
||||
}) = _AppNotification;
|
||||
|
||||
factory AppNotification.fromJson(Map<String, dynamic> json) =>
|
||||
_$AppNotificationFromJson(json);
|
||||
}
|
||||
|
||||
// Using riverpod_generator for cleaner provider code
|
||||
@riverpod
|
||||
class AppNotifications extends _$AppNotifications {
|
||||
StreamSubscription? _subscription;
|
||||
|
||||
@override
|
||||
List<AppNotification> build() {
|
||||
ref.onDispose(() {
|
||||
_subscription?.cancel();
|
||||
});
|
||||
|
||||
_initWebSocketListener();
|
||||
return [];
|
||||
}
|
||||
|
||||
void _initWebSocketListener() {
|
||||
final service = ref.read(websocketProvider);
|
||||
_subscription = service.dataStream.listen((packet) {
|
||||
// Handle notification packets
|
||||
if (packet.type == 'notifications.new') {
|
||||
try {
|
||||
final data = SnNotification.fromJson(packet.data!);
|
||||
|
||||
IconData? icon;
|
||||
switch (data.topic) {
|
||||
case 'general':
|
||||
default:
|
||||
icon = Symbols.info;
|
||||
break;
|
||||
}
|
||||
|
||||
addNotification(
|
||||
AppNotification(
|
||||
data: data,
|
||||
icon: icon,
|
||||
createdAt: data.createdAt.toLocal(),
|
||||
duration: const Duration(seconds: 5),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
log('[Notification] Error processing notification: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void addNotification(AppNotification notification) {
|
||||
// Create a new notification with createdAt if not provided
|
||||
final newNotification =
|
||||
notification.createdAt == null
|
||||
? notification.copyWith(createdAt: DateTime.now())
|
||||
: notification;
|
||||
|
||||
// Add to state
|
||||
state = [...state, newNotification];
|
||||
|
||||
// Auto-remove notification after duration
|
||||
final duration = newNotification.duration ?? const Duration(seconds: 5);
|
||||
Future.delayed(duration, () {
|
||||
// Find the notification in the current state
|
||||
final notificationToRemove = state.firstWhereOrNull(
|
||||
(n) => n.data.id == newNotification.data.id,
|
||||
);
|
||||
|
||||
// Only proceed if the notification still exists in state
|
||||
if (notificationToRemove != null) {
|
||||
// Call removeNotification which will handle the animation
|
||||
removeNotification(notificationToRemove);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Map to track notifications that are being animated out
|
||||
final Map<String, bool> _animatingNotifications = {};
|
||||
|
||||
// Map to track which notifications should animate out
|
||||
final Map<String, bool> _animatingOutNotifications = {};
|
||||
|
||||
void removeNotification(AppNotification notification) {
|
||||
final notificationId = notification.data.id;
|
||||
|
||||
// If this notification is already being removed, don't do anything
|
||||
if (_animatingNotifications[notificationId] == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark this notification as being removed
|
||||
_animatingNotifications[notificationId] = true;
|
||||
|
||||
// Remove from state immediately - AnimatedList handles the animation
|
||||
state = state.where((n) => n.data.id != notificationId).toList();
|
||||
|
||||
// Clean up tracking
|
||||
_animatingNotifications.remove(notificationId);
|
||||
_animatingOutNotifications.remove(notificationId);
|
||||
}
|
||||
|
||||
// Helper method to check if a notification should animate out
|
||||
bool isAnimatingOut(String notificationId) {
|
||||
return _animatingOutNotifications[notificationId] == true;
|
||||
}
|
||||
|
||||
// Helper method to manually add a notification for testing
|
||||
void showNotification({
|
||||
required SnNotification data,
|
||||
IconData? icon,
|
||||
Duration? duration,
|
||||
}) {
|
||||
addNotification(
|
||||
AppNotification(
|
||||
data: data,
|
||||
icon: icon,
|
||||
duration: duration,
|
||||
createdAt: data.createdAt,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,190 +0,0 @@
|
||||
// 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 'app_notification.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AppNotification implements DiagnosticableTreeMixin {
|
||||
|
||||
SnNotification get data;@JsonKey(ignore: true) IconData? get icon;@JsonKey(ignore: true) Duration? get duration; DateTime? get createdAt;@JsonKey(ignore: true) bool get isAnimatingOut;
|
||||
/// Create a copy of AppNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AppNotificationCopyWith<AppNotification> get copyWith => _$AppNotificationCopyWithImpl<AppNotification>(this as AppNotification, _$identity);
|
||||
|
||||
/// Serializes this AppNotification to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'AppNotification'))
|
||||
..add(DiagnosticsProperty('data', data))..add(DiagnosticsProperty('icon', icon))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('createdAt', createdAt))..add(DiagnosticsProperty('isAnimatingOut', isAnimatingOut));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppNotification&&(identical(other.data, data) || other.data == data)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isAnimatingOut, isAnimatingOut) || other.isAnimatingOut == isAnimatingOut));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,data,icon,duration,createdAt,isAnimatingOut);
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'AppNotification(data: $data, icon: $icon, duration: $duration, createdAt: $createdAt, isAnimatingOut: $isAnimatingOut)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AppNotificationCopyWith<$Res> {
|
||||
factory $AppNotificationCopyWith(AppNotification value, $Res Function(AppNotification) _then) = _$AppNotificationCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
SnNotification data,@JsonKey(ignore: true) IconData? icon,@JsonKey(ignore: true) Duration? duration, DateTime? createdAt,@JsonKey(ignore: true) bool isAnimatingOut
|
||||
});
|
||||
|
||||
|
||||
$SnNotificationCopyWith<$Res> get data;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AppNotificationCopyWithImpl<$Res>
|
||||
implements $AppNotificationCopyWith<$Res> {
|
||||
_$AppNotificationCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AppNotification _self;
|
||||
final $Res Function(AppNotification) _then;
|
||||
|
||||
/// Create a copy of AppNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? data = null,Object? icon = freezed,Object? duration = freezed,Object? createdAt = freezed,Object? isAnimatingOut = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as SnNotification,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
|
||||
as IconData?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
|
||||
as Duration?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,isAnimatingOut: null == isAnimatingOut ? _self.isAnimatingOut : isAnimatingOut // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
/// Create a copy of AppNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnNotificationCopyWith<$Res> get data {
|
||||
|
||||
return $SnNotificationCopyWith<$Res>(_self.data, (value) {
|
||||
return _then(_self.copyWith(data: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _AppNotification with DiagnosticableTreeMixin implements AppNotification {
|
||||
const _AppNotification({required this.data, @JsonKey(ignore: true) this.icon, @JsonKey(ignore: true) this.duration, this.createdAt = null, @JsonKey(ignore: true) this.isAnimatingOut = false});
|
||||
factory _AppNotification.fromJson(Map<String, dynamic> json) => _$AppNotificationFromJson(json);
|
||||
|
||||
@override final SnNotification data;
|
||||
@override@JsonKey(ignore: true) final IconData? icon;
|
||||
@override@JsonKey(ignore: true) final Duration? duration;
|
||||
@override@JsonKey() final DateTime? createdAt;
|
||||
@override@JsonKey(ignore: true) final bool isAnimatingOut;
|
||||
|
||||
/// Create a copy of AppNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$AppNotificationCopyWith<_AppNotification> get copyWith => __$AppNotificationCopyWithImpl<_AppNotification>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$AppNotificationToJson(this, );
|
||||
}
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'AppNotification'))
|
||||
..add(DiagnosticsProperty('data', data))..add(DiagnosticsProperty('icon', icon))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('createdAt', createdAt))..add(DiagnosticsProperty('isAnimatingOut', isAnimatingOut));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppNotification&&(identical(other.data, data) || other.data == data)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isAnimatingOut, isAnimatingOut) || other.isAnimatingOut == isAnimatingOut));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,data,icon,duration,createdAt,isAnimatingOut);
|
||||
|
||||
@override
|
||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
|
||||
return 'AppNotification(data: $data, icon: $icon, duration: $duration, createdAt: $createdAt, isAnimatingOut: $isAnimatingOut)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$AppNotificationCopyWith<$Res> implements $AppNotificationCopyWith<$Res> {
|
||||
factory _$AppNotificationCopyWith(_AppNotification value, $Res Function(_AppNotification) _then) = __$AppNotificationCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
SnNotification data,@JsonKey(ignore: true) IconData? icon,@JsonKey(ignore: true) Duration? duration, DateTime? createdAt,@JsonKey(ignore: true) bool isAnimatingOut
|
||||
});
|
||||
|
||||
|
||||
@override $SnNotificationCopyWith<$Res> get data;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$AppNotificationCopyWithImpl<$Res>
|
||||
implements _$AppNotificationCopyWith<$Res> {
|
||||
__$AppNotificationCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _AppNotification _self;
|
||||
final $Res Function(_AppNotification) _then;
|
||||
|
||||
/// Create a copy of AppNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? data = null,Object? icon = freezed,Object? duration = freezed,Object? createdAt = freezed,Object? isAnimatingOut = null,}) {
|
||||
return _then(_AppNotification(
|
||||
data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as SnNotification,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
|
||||
as IconData?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
|
||||
as Duration?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,isAnimatingOut: null == isAnimatingOut ? _self.isAnimatingOut : isAnimatingOut // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of AppNotification
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnNotificationCopyWith<$Res> get data {
|
||||
|
||||
return $SnNotificationCopyWith<$Res>(_self.data, (value) {
|
||||
return _then(_self.copyWith(data: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
@ -1,48 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_notification.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_AppNotification _$AppNotificationFromJson(Map<String, dynamic> json) =>
|
||||
_AppNotification(
|
||||
data: SnNotification.fromJson(json['data'] as Map<String, dynamic>),
|
||||
createdAt:
|
||||
json['created_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['created_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AppNotificationToJson(_AppNotification instance) =>
|
||||
<String, dynamic>{
|
||||
'data': instance.data.toJson(),
|
||||
'created_at': instance.createdAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$appNotificationsHash() => r'a7e7e1d1533e329b000d4b294e455b8420ec3c4d';
|
||||
|
||||
/// See also [AppNotifications].
|
||||
@ProviderFor(AppNotifications)
|
||||
final appNotificationsProvider = AutoDisposeNotifierProvider<
|
||||
AppNotifications,
|
||||
List<AppNotification>
|
||||
>.internal(
|
||||
AppNotifications.new,
|
||||
name: r'appNotificationsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$appNotificationsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$AppNotifications = AutoDisposeNotifier<List<AppNotification>>;
|
||||
// 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
|
@ -12,7 +12,6 @@ import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/pods/websocket.dart';
|
||||
import 'package:island/route.dart';
|
||||
import 'package:island/services/responsive.dart';
|
||||
import 'package:island/widgets/app_notification.dart';
|
||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@ -26,24 +25,31 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Add window resize listener for desktop platforms
|
||||
useEffect(() {
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
if (!kIsWeb &&
|
||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
void saveWindowSize() {
|
||||
final size = appWindow.size;
|
||||
final settingsNotifier = ref.read(appSettingsNotifierProvider.notifier);
|
||||
final settingsNotifier = ref.read(
|
||||
appSettingsNotifierProvider.notifier,
|
||||
);
|
||||
settingsNotifier.setWindowSize(size);
|
||||
}
|
||||
|
||||
|
||||
// Save window size when app is about to close
|
||||
WidgetsBinding.instance.addObserver(_WindowSizeObserver(saveWindowSize));
|
||||
|
||||
WidgetsBinding.instance.addObserver(
|
||||
_WindowSizeObserver(saveWindowSize),
|
||||
);
|
||||
|
||||
return () {
|
||||
// Cleanup observer when widget is disposed
|
||||
WidgetsBinding.instance.removeObserver(_WindowSizeObserver(saveWindowSize));
|
||||
WidgetsBinding.instance.removeObserver(
|
||||
_WindowSizeObserver(saveWindowSize),
|
||||
);
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
|
||||
if (!kIsWeb &&
|
||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
final devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||
@ -106,7 +112,6 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
_WebSocketIndicator(),
|
||||
AppNotificationToast(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -114,39 +119,37 @@ class WindowScaffold extends HookConsumerWidget {
|
||||
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Positioned.fill(child: child),
|
||||
_WebSocketIndicator(),
|
||||
AppNotificationToast(),
|
||||
],
|
||||
children: [Positioned.fill(child: child), _WebSocketIndicator()],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WindowSizeObserver extends WidgetsBindingObserver {
|
||||
final VoidCallback onSaveWindowSize;
|
||||
|
||||
|
||||
_WindowSizeObserver(this.onSaveWindowSize);
|
||||
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
|
||||
|
||||
// Save window size when app is paused, detached, or hidden
|
||||
if (state == AppLifecycleState.paused ||
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.detached ||
|
||||
state == AppLifecycleState.hidden) {
|
||||
if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
if (!kIsWeb &&
|
||||
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||
onSaveWindowSize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _WindowSizeObserver && other.onSaveWindowSize == onSaveWindowSize;
|
||||
return other is _WindowSizeObserver &&
|
||||
other.onSaveWindowSize == onSaveWindowSize;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => onSaveWindowSize.hashCode;
|
||||
}
|
||||
|
31
lib/widgets/app_wrapper.dart
Normal file
31
lib/widgets/app_wrapper.dart
Normal file
@ -0,0 +1,31 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/services/notify.dart';
|
||||
import 'package:island/services/sharing_intent.dart';
|
||||
|
||||
@RoutePage()
|
||||
class AppWrapper extends HookConsumerWidget {
|
||||
const AppWrapper({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
useEffect(() {
|
||||
StreamSubscription? ntySubs;
|
||||
Future(() {
|
||||
if (context.mounted) ntySubs = setupNotificationListener(context, ref);
|
||||
});
|
||||
final sharingService = SharingIntentService();
|
||||
sharingService.initialize(context);
|
||||
return () {
|
||||
sharingService.dispose();
|
||||
ntySubs?.cancel();
|
||||
};
|
||||
}, const []);
|
||||
|
||||
return AutoRouter();
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/call.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/chat/call_participant_tile.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@ -175,14 +176,7 @@ class CallControlsBar extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${'failedToEnumerateDevices'.tr()}: $e'),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,6 +11,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/models/file.dart';
|
||||
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:path/path.dart' show extension;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
@ -215,14 +216,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
// Show error message
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to save image: $e'),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
showErrorAlert(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,7 +94,6 @@ class MarkdownTextContent extends HookConsumerWidget {
|
||||
});
|
||||
} else {
|
||||
showSnackBar(
|
||||
context,
|
||||
'brokenLink'.tr(args: [href]),
|
||||
action: SnackBarAction(
|
||||
label: 'copyToClipboard'.tr(),
|
||||
|
27
lib/widgets/navigation/conditional_bottom_nav.dart
Normal file
27
lib/widgets/navigation/conditional_bottom_nav.dart
Normal file
@ -0,0 +1,27 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
import 'package:island/screens/tabs.dart';
|
||||
|
||||
class ConditionalBottomNav extends HookConsumerWidget {
|
||||
final Widget child;
|
||||
|
||||
const ConditionalBottomNav({super.key, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentRouteName = ref.watch(currentRouteProvider);
|
||||
|
||||
const mainTabRoutes = {
|
||||
ExploreRoute.name,
|
||||
ChatListRoute.name,
|
||||
RealmListRoute.name,
|
||||
AccountRoute.name,
|
||||
};
|
||||
|
||||
debugPrint(currentRouteName);
|
||||
final shouldShowBottomNav = mainTabRoutes.contains(currentRouteName);
|
||||
|
||||
return shouldShowBottomNav ? child : const SizedBox.shrink();
|
||||
}
|
||||
}
|
@ -106,7 +106,9 @@ class _PaymentContent extends ConsumerStatefulWidget {
|
||||
|
||||
class _PaymentContentState extends ConsumerState<_PaymentContent> {
|
||||
static const String _pinStorageKey = 'app_pin_code';
|
||||
static final _secureStorage = FlutterSecureStorage();
|
||||
static final _secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
);
|
||||
|
||||
final LocalAuthentication _localAuth = LocalAuthentication();
|
||||
|
||||
@ -279,7 +281,7 @@ class _PaymentContentState extends ConsumerState<_PaymentContent> {
|
||||
_isPinMode = true;
|
||||
});
|
||||
if (message != null && message.isNotEmpty) {
|
||||
showSnackBar(context, message);
|
||||
showSnackBar(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -15,6 +14,7 @@ import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:pasteboard/pasteboard.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
class ComposeState {
|
||||
final ValueNotifier<List<UniversalFile>> attachments;
|
||||
@ -40,10 +40,10 @@ class ComposeState {
|
||||
required this.draftId,
|
||||
});
|
||||
|
||||
void startAutoSave(WidgetRef ref) {
|
||||
void startAutoSave(WidgetRef ref, {int postType = 0}) {
|
||||
_autoSaveTimer?.cancel();
|
||||
_autoSaveTimer = Timer.periodic(const Duration(seconds: 3), (_) {
|
||||
ComposeLogic.saveDraft(ref, this);
|
||||
ComposeLogic.saveDraftWithoutUpload(ref, this, postType: postType);
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,9 +96,11 @@ class ComposeLogic {
|
||||
);
|
||||
}
|
||||
|
||||
static ComposeState createStateFromDraft(ComposeDraftModel draft) {
|
||||
static ComposeState createStateFromDraft(SnPost draft) {
|
||||
return ComposeState(
|
||||
attachments: ValueNotifier<List<UniversalFile>>([]),
|
||||
attachments: ValueNotifier<List<UniversalFile>>(
|
||||
draft.attachments.map((e) => UniversalFile.fromAttachment(e)).toList(),
|
||||
),
|
||||
titleController: TextEditingController(text: draft.title),
|
||||
descriptionController: TextEditingController(text: draft.description),
|
||||
contentController: TextEditingController(text: draft.content),
|
||||
@ -110,29 +112,257 @@ class ComposeLogic {
|
||||
);
|
||||
}
|
||||
|
||||
static Future<void> saveDraft(
|
||||
WidgetRef ref,
|
||||
ComposeState state, {
|
||||
int postType = 0,
|
||||
}) async {
|
||||
final hasContent =
|
||||
state.titleController.text.trim().isNotEmpty ||
|
||||
state.descriptionController.text.trim().isNotEmpty ||
|
||||
state.contentController.text.trim().isNotEmpty;
|
||||
final hasAttachments = state.attachments.value.isNotEmpty;
|
||||
|
||||
if (!hasContent && !hasAttachments) {
|
||||
return; // Don't save empty posts
|
||||
}
|
||||
|
||||
static Future<void> saveDraft(WidgetRef ref, ComposeState state) async {
|
||||
try {
|
||||
// Check if the auto-save timer is still active (widget not disposed)
|
||||
if (state._autoSaveTimer == null) {
|
||||
return; // Widget has been disposed, don't save
|
||||
return;
|
||||
}
|
||||
|
||||
final draft = ComposeDraftModel(
|
||||
// Upload any local attachments first
|
||||
final baseUrl = ref.watch(serverUrlProvider);
|
||||
final token = await getToken(ref.watch(tokenProvider));
|
||||
if (token == null) throw ArgumentError('Token is null');
|
||||
|
||||
for (int i = 0; i < state.attachments.value.length; i++) {
|
||||
final attachment = state.attachments.value[i];
|
||||
if (attachment.data is! SnCloudFile) {
|
||||
try {
|
||||
final cloudFile =
|
||||
await putMediaToCloud(
|
||||
fileData: attachment,
|
||||
atk: token,
|
||||
baseUrl: baseUrl,
|
||||
filename:
|
||||
attachment.data.name ??
|
||||
(postType == 1 ? 'Article media' : 'Post media'),
|
||||
mimetype:
|
||||
attachment.data.mimeType ??
|
||||
ComposeLogic.getMimeTypeFromFileType(attachment.type),
|
||||
).future;
|
||||
if (cloudFile != null) {
|
||||
// Update attachments list with cloud file
|
||||
final clone = List.of(state.attachments.value);
|
||||
clone[i] = UniversalFile(data: cloudFile, type: attachment.type);
|
||||
state.attachments.value = clone;
|
||||
}
|
||||
} catch (err) {
|
||||
log('[ComposeLogic] Failed to upload attachment: $err');
|
||||
// Continue with other attachments even if one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final draft = SnPost(
|
||||
id: state.draftId,
|
||||
title: state.titleController.text,
|
||||
description: state.descriptionController.text,
|
||||
content: state.contentController.text,
|
||||
attachments: state.attachments.value,
|
||||
language: null,
|
||||
editedAt: null,
|
||||
publishedAt: DateTime.now(),
|
||||
visibility: state.visibility.value,
|
||||
lastModified: DateTime.now(),
|
||||
content: state.contentController.text,
|
||||
type: postType,
|
||||
meta: null,
|
||||
viewsUnique: 0,
|
||||
viewsTotal: 0,
|
||||
upvotes: 0,
|
||||
downvotes: 0,
|
||||
repliesCount: 0,
|
||||
threadedPostId: null,
|
||||
threadedPost: null,
|
||||
repliedPostId: null,
|
||||
repliedPost: null,
|
||||
forwardedPostId: null,
|
||||
forwardedPost: null,
|
||||
attachments:
|
||||
state.attachments.value
|
||||
.map((e) => e.data)
|
||||
.whereType<SnCloudFile>()
|
||||
.toList(),
|
||||
publisher: SnPublisher(
|
||||
id: '',
|
||||
type: 0,
|
||||
name: '',
|
||||
nick: '',
|
||||
picture: null,
|
||||
background: null,
|
||||
account: null,
|
||||
accountId: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
realmId: null,
|
||||
verification: null,
|
||||
),
|
||||
reactions: [],
|
||||
tags: [],
|
||||
categories: [],
|
||||
collections: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
);
|
||||
|
||||
await ref.read(composeStorageNotifierProvider.notifier).saveDraft(draft);
|
||||
} catch (e) {
|
||||
log('[ComposeLogic] Failed to save draft, error: $e');
|
||||
// Silently fail for auto-save to avoid disrupting user experience
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> saveDraftWithoutUpload(
|
||||
WidgetRef ref,
|
||||
ComposeState state, {
|
||||
int postType = 0,
|
||||
}) async {
|
||||
final hasContent =
|
||||
state.titleController.text.trim().isNotEmpty ||
|
||||
state.descriptionController.text.trim().isNotEmpty ||
|
||||
state.contentController.text.trim().isNotEmpty;
|
||||
final hasAttachments = state.attachments.value.isNotEmpty;
|
||||
|
||||
if (!hasContent && !hasAttachments) {
|
||||
return; // Don't save empty posts
|
||||
}
|
||||
|
||||
try {
|
||||
if (state._autoSaveTimer == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final draft = SnPost(
|
||||
id: state.draftId,
|
||||
title: state.titleController.text,
|
||||
description: state.descriptionController.text,
|
||||
language: null,
|
||||
editedAt: null,
|
||||
publishedAt: DateTime.now(),
|
||||
visibility: state.visibility.value,
|
||||
content: state.contentController.text,
|
||||
type: postType,
|
||||
meta: null,
|
||||
viewsUnique: 0,
|
||||
viewsTotal: 0,
|
||||
upvotes: 0,
|
||||
downvotes: 0,
|
||||
repliesCount: 0,
|
||||
threadedPostId: null,
|
||||
threadedPost: null,
|
||||
repliedPostId: null,
|
||||
repliedPost: null,
|
||||
forwardedPostId: null,
|
||||
forwardedPost: null,
|
||||
attachments:
|
||||
state.attachments.value
|
||||
.map((e) => e.data)
|
||||
.whereType<SnCloudFile>()
|
||||
.toList(),
|
||||
publisher: SnPublisher(
|
||||
id: '',
|
||||
type: 0,
|
||||
name: '',
|
||||
nick: '',
|
||||
picture: null,
|
||||
background: null,
|
||||
account: null,
|
||||
accountId: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
realmId: null,
|
||||
verification: null,
|
||||
),
|
||||
reactions: [],
|
||||
tags: [],
|
||||
categories: [],
|
||||
collections: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
);
|
||||
|
||||
await ref.read(composeStorageNotifierProvider.notifier).saveDraft(draft);
|
||||
} catch (e) {
|
||||
log('[ComposeLogic] Failed to save draft without upload, error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> saveDraftManually(
|
||||
WidgetRef ref,
|
||||
ComposeState state,
|
||||
BuildContext context,
|
||||
) async {
|
||||
try {
|
||||
final draft = SnPost(
|
||||
id: state.draftId,
|
||||
title: state.titleController.text,
|
||||
description: state.descriptionController.text,
|
||||
language: null,
|
||||
editedAt: null,
|
||||
publishedAt: DateTime.now(),
|
||||
visibility: state.visibility.value,
|
||||
content: state.contentController.text,
|
||||
type: 0,
|
||||
meta: null,
|
||||
viewsUnique: 0,
|
||||
viewsTotal: 0,
|
||||
upvotes: 0,
|
||||
downvotes: 0,
|
||||
repliesCount: 0,
|
||||
threadedPostId: null,
|
||||
threadedPost: null,
|
||||
repliedPostId: null,
|
||||
repliedPost: null,
|
||||
forwardedPostId: null,
|
||||
forwardedPost: null,
|
||||
attachments: [], // TODO: Handle attachments
|
||||
publisher: SnPublisher(
|
||||
id: '',
|
||||
type: 0,
|
||||
name: '',
|
||||
nick: '',
|
||||
picture: null,
|
||||
background: null,
|
||||
account: null,
|
||||
accountId: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
realmId: null,
|
||||
verification: null,
|
||||
),
|
||||
reactions: [],
|
||||
tags: [],
|
||||
categories: [],
|
||||
collections: [],
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
);
|
||||
|
||||
await ref.read(composeStorageNotifierProvider.notifier).saveDraft(draft);
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBar('draftSaved'.tr());
|
||||
}
|
||||
} catch (e) {
|
||||
log('[ComposeLogic] Failed to save draft manually, error: $e');
|
||||
if (context.mounted) {
|
||||
showSnackBar('draftSaveFailed'.tr());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +376,7 @@ class ComposeLogic {
|
||||
}
|
||||
}
|
||||
|
||||
static Future<ComposeDraftModel?> loadDraft(WidgetRef ref, String draftId) async {
|
||||
static Future<SnPost?> loadDraft(WidgetRef ref, String draftId) async {
|
||||
try {
|
||||
return ref
|
||||
.read(composeStorageNotifierProvider.notifier)
|
||||
@ -282,6 +512,20 @@ class ComposeLogic {
|
||||
}) async {
|
||||
if (state.submitting.value) return;
|
||||
|
||||
// Don't submit empty posts (no content and no attachments)
|
||||
final hasContent =
|
||||
state.titleController.text.trim().isNotEmpty ||
|
||||
state.descriptionController.text.trim().isNotEmpty ||
|
||||
state.contentController.text.trim().isNotEmpty;
|
||||
final hasAttachments = state.attachments.value.isNotEmpty;
|
||||
|
||||
if (!hasContent && !hasAttachments) {
|
||||
if (context.mounted) {
|
||||
showSnackBar('postContentEmpty'.tr());
|
||||
}
|
||||
return; // Don't submit empty posts
|
||||
}
|
||||
|
||||
try {
|
||||
state.submitting.value = true;
|
||||
|
||||
@ -329,7 +573,7 @@ class ComposeLogic {
|
||||
if (postType == 1) {
|
||||
// Delete article draft
|
||||
await ref
|
||||
.read(articleStorageNotifierProvider.notifier)
|
||||
.read(composeStorageNotifierProvider.notifier)
|
||||
.deleteDraft(state.draftId);
|
||||
} else {
|
||||
// Delete regular post draft
|
||||
@ -381,7 +625,7 @@ class ComposeLogic {
|
||||
if (isPaste && isModifierPressed) {
|
||||
handlePaste(state);
|
||||
} else if (isSave && isModifierPressed) {
|
||||
saveDraft(ref, state);
|
||||
saveDraftManually(ref, state, context);
|
||||
} else if (isSubmit && isModifierPressed && !state.submitting.value) {
|
||||
performAction(
|
||||
ref,
|
||||
|
@ -7,208 +7,168 @@ import 'package:island/services/compose_storage_db.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class DraftManagerSheet extends HookConsumerWidget {
|
||||
final bool isArticle;
|
||||
final Function(String draftId)? onDraftSelected;
|
||||
|
||||
const DraftManagerSheet({
|
||||
super.key,
|
||||
this.isArticle = false,
|
||||
this.onDraftSelected,
|
||||
});
|
||||
const DraftManagerSheet({super.key, this.onDraftSelected});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
final isLoading = useState(true);
|
||||
|
||||
final drafts =
|
||||
isArticle
|
||||
? ref.watch(articleStorageNotifierProvider)
|
||||
: ref.watch(composeStorageNotifierProvider);
|
||||
final drafts = ref.watch(composeStorageNotifierProvider);
|
||||
|
||||
final sortedDrafts = useMemoized(() {
|
||||
if (isArticle) {
|
||||
final draftList = drafts.values.cast<ArticleDraftModel>().toList();
|
||||
draftList.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||
return draftList;
|
||||
} else {
|
||||
final draftList = drafts.values.cast<ComposeDraftModel>().toList();
|
||||
draftList.sort((a, b) => b.lastModified.compareTo(a.lastModified));
|
||||
return draftList;
|
||||
}
|
||||
// Track loading state based on drafts being loaded
|
||||
useEffect(() {
|
||||
// Set loading to false after drafts are loaded
|
||||
// We consider drafts loaded when the provider has been initialized
|
||||
Future.microtask(() {
|
||||
if (isLoading.value) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
});
|
||||
return null;
|
||||
}, [drafts]);
|
||||
|
||||
final sortedDrafts = useMemoized(
|
||||
() {
|
||||
final draftList = drafts.values.toList();
|
||||
draftList.sort((a, b) => b.updatedAt!.compareTo(a.updatedAt!));
|
||||
return draftList;
|
||||
},
|
||||
[
|
||||
drafts.length,
|
||||
drafts.values.map((e) => e.updatedAt!.millisecondsSinceEpoch).join(),
|
||||
],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(isArticle ? 'articleDrafts'.tr() : 'postDrafts'.tr()),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (sortedDrafts.isEmpty)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.draft,
|
||||
size: 64,
|
||||
color: colorScheme.onSurface.withOpacity(0.3),
|
||||
appBar: AppBar(title: Text('drafts'.tr())),
|
||||
body:
|
||||
isLoading.value
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
children: [
|
||||
if (sortedDrafts.isEmpty)
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.draft,
|
||||
size: 64,
|
||||
color: colorScheme.onSurface.withOpacity(0.3),
|
||||
),
|
||||
const Gap(16),
|
||||
Text(
|
||||
'noDrafts'.tr(),
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: sortedDrafts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final draft = sortedDrafts[index];
|
||||
return _DraftItem(
|
||||
draft: draft,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
onDraftSelected?.call(draft.id);
|
||||
},
|
||||
onDelete: () async {
|
||||
await ref
|
||||
.read(composeStorageNotifierProvider.notifier)
|
||||
.deleteDraft(draft.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
Text(
|
||||
'noDrafts'.tr(),
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withOpacity(0.6),
|
||||
if (sortedDrafts.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text('clearAllDrafts'.tr()),
|
||||
content: Text(
|
||||
'clearAllDraftsConfirm'.tr(),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.of(
|
||||
context,
|
||||
).pop(false),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.of(
|
||||
context,
|
||||
).pop(true),
|
||||
child: Text('confirm'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
await ref
|
||||
.read(
|
||||
composeStorageNotifierProvider.notifier,
|
||||
)
|
||||
.clearAllDrafts();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.delete_sweep),
|
||||
label: Text('clearAll'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: sortedDrafts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final draft = sortedDrafts[index];
|
||||
return _DraftItem(
|
||||
draft: draft,
|
||||
isArticle: isArticle,
|
||||
onTap: () {
|
||||
Navigator.of(context).pop();
|
||||
final draftId =
|
||||
isArticle
|
||||
? (draft as ArticleDraftModel).id
|
||||
: (draft as ComposeDraftModel).id;
|
||||
onDraftSelected?.call(draftId);
|
||||
},
|
||||
onDelete: () async {
|
||||
final draftId =
|
||||
isArticle
|
||||
? (draft as ArticleDraftModel).id
|
||||
: (draft as ComposeDraftModel).id;
|
||||
if (isArticle) {
|
||||
await ref
|
||||
.read(articleStorageNotifierProvider.notifier)
|
||||
.deleteDraft(draftId);
|
||||
} else {
|
||||
await ref
|
||||
.read(composeStorageNotifierProvider.notifier)
|
||||
.deleteDraft(draftId);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (sortedDrafts.isNotEmpty) ...[
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: Text('clearAllDrafts'.tr()),
|
||||
content: Text('clearAllDraftsConfirm'.tr()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.of(context).pop(false),
|
||||
child: Text('cancel'.tr()),
|
||||
),
|
||||
TextButton(
|
||||
onPressed:
|
||||
() => Navigator.of(context).pop(true),
|
||||
child: Text('confirm'.tr()),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
if (isArticle) {
|
||||
await ref
|
||||
.read(articleStorageNotifierProvider.notifier)
|
||||
.clearAllDrafts();
|
||||
} else {
|
||||
await ref
|
||||
.read(composeStorageNotifierProvider.notifier)
|
||||
.clearAllDrafts();
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Symbols.delete_sweep),
|
||||
label: Text('clearAll'.tr()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DraftItem extends StatelessWidget {
|
||||
final dynamic draft; // ComposeDraft or ArticleDraft
|
||||
final bool isArticle;
|
||||
final dynamic draft;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
const _DraftItem({
|
||||
required this.draft,
|
||||
required this.isArticle,
|
||||
this.onTap,
|
||||
this.onDelete,
|
||||
});
|
||||
const _DraftItem({required this.draft, this.onTap, this.onDelete});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
final String title;
|
||||
final String content;
|
||||
final DateTime lastModified;
|
||||
final String visibility;
|
||||
|
||||
if (isArticle) {
|
||||
final articleDraft = draft as ArticleDraftModel;
|
||||
title =
|
||||
articleDraft.title.isNotEmpty ? articleDraft.title : 'untitled'.tr();
|
||||
content =
|
||||
articleDraft.content.isNotEmpty
|
||||
? articleDraft.content
|
||||
: (articleDraft.description.isNotEmpty
|
||||
? articleDraft.description
|
||||
: 'noContent'.tr());
|
||||
lastModified = articleDraft.lastModified;
|
||||
visibility = _parseArticleVisibility(articleDraft.visibility);
|
||||
} else {
|
||||
final postDraft = draft as ComposeDraftModel;
|
||||
title = postDraft.title.isNotEmpty ? postDraft.title : 'untitled'.tr();
|
||||
content =
|
||||
postDraft.content.isNotEmpty
|
||||
? postDraft.content
|
||||
: (postDraft.description.isNotEmpty
|
||||
? postDraft.description
|
||||
: 'noContent'.tr());
|
||||
lastModified = postDraft.lastModified;
|
||||
visibility = _parseArticleVisibility(postDraft.visibility);
|
||||
}
|
||||
|
||||
final title = draft.title ?? 'untitled'.tr();
|
||||
final content = draft.content ?? (draft.description ?? 'noContent'.tr());
|
||||
final preview =
|
||||
content.length > 100 ? '${content.substring(0, 100)}...' : content;
|
||||
final timeAgo = _formatTimeAgo(lastModified);
|
||||
final timeAgo = _formatTimeAgo(draft.updatedAt!);
|
||||
final visibility = _parseVisibility(draft.visibility);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
@ -223,7 +183,7 @@ class _DraftItem extends StatelessWidget {
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isArticle ? Symbols.article : Symbols.post_add,
|
||||
draft.type == 1 ? Symbols.article : Symbols.post_add,
|
||||
size: 20,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
@ -316,7 +276,7 @@ class _DraftItem extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _parseArticleVisibility(int visibility) {
|
||||
String _parseVisibility(int visibility) {
|
||||
switch (visibility) {
|
||||
case 0:
|
||||
return 'public'.tr();
|
||||
|
@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'dart:math' as math;
|
||||
import 'package:island/models/embed.dart';
|
||||
import 'package:island/models/post.dart';
|
||||
import 'package:island/pods/config.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/pods/userinfo.dart';
|
||||
import 'package:island/route.gr.dart';
|
||||
@ -20,7 +21,9 @@ import 'package:island/widgets/content/cloud_file_collection.dart';
|
||||
import 'package:island/widgets/content/cloud_files.dart';
|
||||
import 'package:island/widgets/content/embed/link.dart';
|
||||
import 'package:island/widgets/content/markdown.dart';
|
||||
import 'package:island/widgets/safety/abuse_report_helper.dart';
|
||||
import 'package:island/widgets/post/post_replies_sheet.dart';
|
||||
import 'package:island/widgets/share/share_sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_context_menu/super_context_menu.dart';
|
||||
@ -124,6 +127,29 @@ class PostItem extends HookConsumerWidget {
|
||||
context.router.push(PostComposeRoute(forwardedPost: item));
|
||||
},
|
||||
),
|
||||
MenuSeparator(),
|
||||
MenuAction(
|
||||
title: 'share'.tr(),
|
||||
image: MenuImage.icon(Symbols.share),
|
||||
callback: () {
|
||||
showShareSheetLink(
|
||||
context: context,
|
||||
link: '${ref.read(serverUrlProvider)}/posts/${item.id}',
|
||||
title: 'sharePost'.tr(),
|
||||
toSystem: true,
|
||||
);
|
||||
},
|
||||
),
|
||||
MenuAction(
|
||||
title: 'abuseReport'.tr(),
|
||||
image: MenuImage.icon(Symbols.flag),
|
||||
callback: () {
|
||||
showAbuseReportSheet(
|
||||
context,
|
||||
resourceIdentifier: 'posts:${item.id}',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
@ -162,8 +188,11 @@ class PostItem extends HookConsumerWidget {
|
||||
Spacer(),
|
||||
Text(
|
||||
isFullPost
|
||||
? item.publishedAt.formatSystem()
|
||||
: item.publishedAt.formatRelative(context),
|
||||
? item.publishedAt?.formatSystem() ?? ''
|
||||
: item.publishedAt?.formatRelative(
|
||||
context,
|
||||
) ??
|
||||
'',
|
||||
).fontSize(11).alignment(Alignment.bottomRight),
|
||||
const Gap(4),
|
||||
],
|
||||
@ -213,12 +242,14 @@ class PostItem extends HookConsumerWidget {
|
||||
content: item.content!,
|
||||
linesMargin:
|
||||
item.type == 0
|
||||
? EdgeInsets.only(bottom: 4)
|
||||
? EdgeInsets.only(bottom: 8)
|
||||
: null,
|
||||
),
|
||||
// Show truncation hint if post is truncated
|
||||
if (item.isTruncated && !isFullPost)
|
||||
_PostTruncateHint(),
|
||||
_PostTruncateHint().padding(
|
||||
bottom: item.attachments.isNotEmpty ? 8 : null,
|
||||
),
|
||||
if ((item.repliedPost != null ||
|
||||
item.forwardedPost != null) &&
|
||||
showReferencePost)
|
||||
@ -234,7 +265,7 @@ class PostItem extends HookConsumerWidget {
|
||||
MediaQuery.of(context).size.width * 0.9,
|
||||
kWideScreenWidth - 160,
|
||||
),
|
||||
).padding(top: 4),
|
||||
),
|
||||
// Render embed links
|
||||
if (item.meta?['embeds'] != null)
|
||||
...((item.meta!['embeds'] as List<dynamic>)
|
||||
@ -248,7 +279,8 @@ class PostItem extends HookConsumerWidget {
|
||||
MediaQuery.of(context).size.width * 0.85,
|
||||
kWideScreenWidth - 160,
|
||||
),
|
||||
).padding(top: 4),
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
@ -323,7 +355,6 @@ Widget _buildReferencePost(BuildContext context, SnPost item) {
|
||||
final isReply = item.repliedPost != null;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 8, bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant.withOpacity(0.5),
|
||||
@ -702,12 +733,16 @@ class _PostTruncateHint extends StatelessWidget {
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
SizedBox(width: isCompact ? 4 : 6),
|
||||
Text(
|
||||
'postTruncated'.tr(),
|
||||
style: TextStyle(
|
||||
fontSize: isCompact ? 10 : 12,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontStyle: FontStyle.italic,
|
||||
Flexible(
|
||||
child: Text(
|
||||
'postTruncated'.tr(),
|
||||
style: TextStyle(
|
||||
fontSize: isCompact ? 10 : 12,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
SizedBox(width: isCompact ? 3 : 4),
|
||||
|
@ -153,7 +153,7 @@ class PostItemCreator extends HookConsumerWidget {
|
||||
),
|
||||
const Gap(8),
|
||||
Text(
|
||||
item.publishedAt.formatSystem(),
|
||||
item.publishedAt?.formatSystem() ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
@ -291,7 +291,7 @@ class PostItemCreator extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Created: ${item.createdAt.formatSystem()}',
|
||||
'Created: ${item.createdAt?.formatSystem() ?? ''}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
|
24
lib/widgets/safety/abuse_report_helper.dart
Normal file
24
lib/widgets/safety/abuse_report_helper.dart
Normal file
@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:island/widgets/safety/abuse_report_sheet.dart';
|
||||
|
||||
/// Helper function to show the safety report sheet
|
||||
///
|
||||
/// [context] - The build context
|
||||
/// [resourceIdentifier] - The identifier of the resource being reported (e.g., post ID, user ID, etc.)
|
||||
/// [initialReason] - Optional initial reason text to pre-fill the form
|
||||
Future<void> showAbuseReportSheet(
|
||||
BuildContext context, {
|
||||
required String resourceIdentifier,
|
||||
String? initialReason,
|
||||
}) {
|
||||
return showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder:
|
||||
(context) => AbuseReportSheet(
|
||||
resourceIdentifier: resourceIdentifier,
|
||||
initialReason: initialReason,
|
||||
),
|
||||
);
|
||||
}
|
184
lib/widgets/safety/abuse_report_sheet.dart
Normal file
184
lib/widgets/safety/abuse_report_sheet.dart
Normal file
@ -0,0 +1,184 @@
|
||||
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:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/pods/network.dart';
|
||||
import 'package:island/widgets/alert.dart';
|
||||
import 'package:island/widgets/content/sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class AbuseReportSheet extends HookConsumerWidget {
|
||||
final String resourceIdentifier;
|
||||
final String? initialReason;
|
||||
|
||||
const AbuseReportSheet({
|
||||
super.key,
|
||||
required this.resourceIdentifier,
|
||||
this.initialReason,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final reasonController = useTextEditingController(
|
||||
text: initialReason ?? '',
|
||||
);
|
||||
final selectedType = useState<int>(0);
|
||||
final isSubmitting = useState<bool>(false);
|
||||
|
||||
final reportTypes = [
|
||||
{'value': 0, 'label': 'abuseReportTypeCopyright'.tr()},
|
||||
{'value': 1, 'label': 'abuseReportTypeHarassment'.tr()},
|
||||
{'value': 2, 'label': 'abuseReportTypeImpersonation'.tr()},
|
||||
{'value': 3, 'label': 'abuseReportTypeOffensiveContent'.tr()},
|
||||
{'value': 4, 'label': 'abuseReportTypeSpam'.tr()},
|
||||
{'value': 5, 'label': 'abuseReportTypePrivacyViolation'.tr()},
|
||||
{'value': 6, 'label': 'abuseReportTypeIllegalContent'.tr()},
|
||||
{'value': 7, 'label': 'abuseReportTypeOther'.tr()},
|
||||
];
|
||||
|
||||
Future<void> submitReport() async {
|
||||
isSubmitting.value = true;
|
||||
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post(
|
||||
'/safety/reports',
|
||||
data: {
|
||||
'resource_identifier': resourceIdentifier,
|
||||
'type': selectedType.value,
|
||||
'reason': reasonController.text.trim(),
|
||||
},
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder:
|
||||
(contextDialog) => AlertDialog(
|
||||
icon: const Icon(
|
||||
Icons.check_circle,
|
||||
color: Colors.green,
|
||||
size: 36,
|
||||
),
|
||||
title: Text('abuseReportSuccessTitle'.tr()),
|
||||
content: Text('abuseReportSuccess'.tr()),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(contextDialog).pop();
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return SheetScaffold(
|
||||
titleText: 'abuseReportTitle'.tr(),
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Information text
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.info,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const Gap(8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'abuseReportDescription'.tr(),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
|
||||
// Report type selection
|
||||
Text(
|
||||
'abuseReportType'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Gap(12),
|
||||
...reportTypes.map((type) {
|
||||
return RadioListTile<int>(
|
||||
value: type['value'] as int,
|
||||
groupValue: selectedType.value,
|
||||
onChanged: (value) => selectedType.value = value!,
|
||||
title: Text(type['label'] as String),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
visualDensity: VisualDensity.compact,
|
||||
);
|
||||
}),
|
||||
const Gap(24),
|
||||
|
||||
// Reason text field
|
||||
Text(
|
||||
'abuseReportReason'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const Gap(8),
|
||||
TextField(
|
||||
controller: reasonController,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'abuseReportReasonHint'.tr(),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
),
|
||||
const Gap(24),
|
||||
|
||||
// Submit button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: isSubmitting.value ? null : submitReport,
|
||||
child:
|
||||
isSubmitting.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text('abuseReportSubmit'.tr()),
|
||||
),
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
1295
lib/widgets/share/share_sheet.dart
Normal file
1295
lib/widgets/share/share_sheet.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,7 @@
|
||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
|
||||
#include <flutter_secure_storage/flutter_secure_storage_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <flutter_timezone/flutter_timezone_plugin.h>
|
||||
#include <flutter_udid/flutter_udid_plugin.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
@ -33,9 +33,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) flutter_platform_alert_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin");
|
||||
flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStoragePlugin");
|
||||
flutter_secure_storage_plugin_register_with_registrar(flutter_secure_storage_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_timezone_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterTimezonePlugin");
|
||||
flutter_timezone_plugin_register_with_registrar(flutter_timezone_registrar);
|
||||
|
@ -6,7 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_linux
|
||||
file_selector_linux
|
||||
flutter_platform_alert
|
||||
flutter_secure_storage
|
||||
flutter_secure_storage_linux
|
||||
flutter_timezone
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
|
@ -14,6 +14,7 @@ import firebase_core
|
||||
import firebase_messaging
|
||||
import flutter_inappwebview_macos
|
||||
import flutter_platform_alert
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_timezone
|
||||
import flutter_udid
|
||||
import flutter_webrtc
|
||||
@ -27,6 +28,7 @@ import package_info_plus
|
||||
import pasteboard
|
||||
import path_provider_foundation
|
||||
import record_macos
|
||||
import share_plus
|
||||
import shared_preferences_foundation
|
||||
import sign_in_with_apple
|
||||
import sqflite_darwin
|
||||
@ -46,6 +48,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
|
||||
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
|
||||
FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
|
||||
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
@ -59,6 +62,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SignInWithApplePlugin.register(with: registry.registrar(forPlugin: "SignInWithApplePlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
|
@ -118,6 +118,8 @@ PODS:
|
||||
- record_macos (1.0.0):
|
||||
- FlutterMacOS
|
||||
- SAMKeychain (1.5.3)
|
||||
- share_plus (0.0.1):
|
||||
- FlutterMacOS
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@ -183,6 +185,7 @@ DEPENDENCIES:
|
||||
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sign_in_with_apple (from `Flutter/ephemeral/.symlinks/plugins/sign_in_with_apple/macos`)
|
||||
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
@ -257,6 +260,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||
record_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||
share_plus:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos
|
||||
shared_preferences_foundation:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||
sign_in_with_apple:
|
||||
@ -310,6 +315,7 @@ SPEC CHECKSUMS:
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
record_macos: 295d70bd5fb47145df78df7b80e6697cd18403c0
|
||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
|
||||
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
|
||||
sign_in_with_apple: 6673c03c9e3643f6c8d33601943fbfa9ae99f94e
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
|
@ -11,4 +11,4 @@ PRODUCT_NAME = Solian
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.solsynth.solian
|
||||
|
||||
// The copyright displayed in application information
|
||||
PRODUCT_COPYRIGHT = Copyright © 2025 Solsynth LLC. All rights reserved.
|
||||
PRODUCT_COPYRIGHT = Copyright © 2025 Solsynth. All rights reserved.
|
||||
|
84
pubspec.lock
84
pubspec.lock
@ -895,10 +895,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "9f3dd2ac3b6875b0fde5b04734789c3ef35ba3965c18e99dd564a7a2f8056df6"
|
||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.2.1"
|
||||
version: "9.2.4"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1177,10 +1217,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1701,6 +1741,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.0"
|
||||
receive_sharing_intent:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: receive_sharing_intent
|
||||
sha256: ec76056e4d258ad708e76d85591d933678625318e411564dcb9059048ca3a593
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
record:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1841,10 +1889,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_brightness_android
|
||||
sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e"
|
||||
sha256: fb5fa43cb89d0c9b8534556c427db1e97e46594ac5d66ebdcf16063b773d54ed
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "2.1.2"
|
||||
screen_brightness_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1869,6 +1917,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.2"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.0.0"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -2258,6 +2322,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
top_snackbar_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: top_snackbar_flutter
|
||||
sha256: ad3f93062450e8c7db97b271d405c180536408cc2be4380a59da7022eb1d750c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -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+106
|
||||
version: 3.0.0+107
|
||||
|
||||
environment:
|
||||
sdk: ^3.7.2
|
||||
@ -117,8 +117,11 @@ dependencies:
|
||||
flutter_svg: ^2.1.0
|
||||
native_exif: ^0.6.2
|
||||
local_auth: ^2.3.0
|
||||
flutter_secure_storage: ^4.2.1
|
||||
flutter_secure_storage: ^9.2.4
|
||||
flutter_math_fork: ^0.7.4
|
||||
share_plus: ^11.0.0
|
||||
receive_sharing_intent: ^1.8.1
|
||||
top_snackbar_flutter: ^3.3.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
|
||||
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
|
||||
#include <flutter_udid/flutter_udid_plugin_c_api.h>
|
||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||
@ -23,6 +24,7 @@
|
||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||
#include <pasteboard/pasteboard_plugin.h>
|
||||
#include <record_windows/record_windows_plugin_c_api.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
|
||||
#include <super_native_extensions/super_native_extensions_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
@ -41,6 +43,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
|
||||
FlutterPlatformAlertPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterPlatformAlertPlugin"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
FlutterTimezonePluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
|
||||
FlutterUdidPluginCApiRegisterWithRegistrar(
|
||||
@ -63,6 +67,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
|
||||
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
|
||||
SuperNativeExtensionsPluginCApiRegisterWithRegistrar(
|
||||
|
@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
firebase_core
|
||||
flutter_inappwebview_windows
|
||||
flutter_platform_alert
|
||||
flutter_secure_storage_windows
|
||||
flutter_timezone
|
||||
flutter_udid
|
||||
flutter_webrtc
|
||||
@ -20,6 +21,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
media_kit_video
|
||||
pasteboard
|
||||
record_windows
|
||||
share_plus
|
||||
sqlite3_flutter_libs
|
||||
super_native_extensions
|
||||
url_launcher_windows
|
||||
|
Reference in New Issue
Block a user