Compare commits
28 Commits
f542d9fa97
...
3.3.0+145
| Author | SHA1 | Date | |
|---|---|---|---|
|
5ebefae961
|
|||
|
d4758674bb
|
|||
|
f5f1ddc0ea
|
|||
|
2720b59485
|
|||
|
29b1ac7fce
|
|||
|
83ca5551ad
|
|||
| 611cb024a9 | |||
|
74fb56891d
|
|||
|
ac4fa5eb85
|
|||
|
8857718709
|
|||
|
dd17b2b9c1
|
|||
|
848439f664
|
|||
|
f83117424d
|
|||
|
8c19c32c76
|
|||
|
d62b2bed80
|
|||
|
5a23eb1768
|
|||
|
5f6e4763d3
|
|||
|
580c36fb89
|
|||
|
6c25af3b30
|
|||
|
a1da72d447
|
|||
|
ab4120cc22
|
|||
|
52eff0fa25
|
|||
|
beeb28abf2
|
|||
|
c0ab3837ac
|
|||
|
59d38c0d8d
|
|||
|
bd2247ce86
|
|||
|
da2d3f7f17
|
|||
|
7497b77384
|
@@ -43,6 +43,16 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- App protocol -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST -->
|
||||||
|
<data android:scheme="solian" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Deeplinking -->
|
<!-- Deeplinking -->
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|||||||
@@ -163,6 +163,7 @@
|
|||||||
"accountConnectionProviderDiscord": "Discord",
|
"accountConnectionProviderDiscord": "Discord",
|
||||||
"accountConnectionProviderAfdian": "Afdian",
|
"accountConnectionProviderAfdian": "Afdian",
|
||||||
"accountConnectionProviderSpotify": "Spotify",
|
"accountConnectionProviderSpotify": "Spotify",
|
||||||
|
"accountConnectionProviderSteam": "Steam",
|
||||||
"checkIn": "Check In",
|
"checkIn": "Check In",
|
||||||
"checkInNone": "Not checked-in yet",
|
"checkInNone": "Not checked-in yet",
|
||||||
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
"checkInNoneHint": "Get your fortune tips and daily rewards by checking in.",
|
||||||
@@ -1309,5 +1310,15 @@
|
|||||||
"presenceTypeGaming": "Playing",
|
"presenceTypeGaming": "Playing",
|
||||||
"presenceTypeMusic": "Listening to Music",
|
"presenceTypeMusic": "Listening to Music",
|
||||||
"presenceTypeWorkout": "Working out",
|
"presenceTypeWorkout": "Working out",
|
||||||
"articleCompose": "Compose Article"
|
"articleCompose": "Compose Article",
|
||||||
|
"backToHub": "Back to Hub",
|
||||||
|
"advancedFilters": "Advanced Filters",
|
||||||
|
"searchPosts": "Search Posts",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"fromDate": "From Date",
|
||||||
|
"toDate": "To Date",
|
||||||
|
"popularity": "Popularity",
|
||||||
|
"descendingOrder": "Descending Order",
|
||||||
|
"selectDate": "Select Date",
|
||||||
|
"pinnedPosts": "Pinned Posts"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,11 +158,11 @@
|
|||||||
"checkIn": "签到",
|
"checkIn": "签到",
|
||||||
"checkInNone": "尚未签到",
|
"checkInNone": "尚未签到",
|
||||||
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
"checkInNoneHint": "通过签到获取您的财富提示和每日奖励。",
|
||||||
"checkInResultLevel0": "最差运气",
|
"checkInResultLevel0": "大凶",
|
||||||
"checkInResultLevel1": "坏运气",
|
"checkInResultLevel1": "凶",
|
||||||
"checkInResultLevel2": "一个普通的日常",
|
"checkInResultLevel2": "中平",
|
||||||
"checkInResultLevel3": "好运",
|
"checkInResultLevel3": "吉",
|
||||||
"checkInResultLevel4": "最佳运气",
|
"checkInResultLevel4": "大吉",
|
||||||
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
"checkInActivityTitle": "{} 在 {} 签到并获得了 {}",
|
||||||
"eventCalander": "活动日历",
|
"eventCalander": "活动日历",
|
||||||
"eventCalanderEmpty": "该日无活动。",
|
"eventCalanderEmpty": "该日无活动。",
|
||||||
|
|||||||
BIN
assets/icons/icon-tray.png
Normal file
BIN
assets/icons/icon-tray.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
1
assets/images/oidc/steam.svg
Normal file
1
assets/images/oidc/steam.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg width="2471" height="2500" viewBox="0 0 256 259" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M127.779 0C60.42 0 5.24 52.412 0 119.014l68.724 28.674a35.812 35.812 0 0 1 20.426-6.366c.682 0 1.356.019 2.02.056l30.566-44.71v-.626c0-26.903 21.69-48.796 48.353-48.796 26.662 0 48.352 21.893 48.352 48.796 0 26.902-21.69 48.804-48.352 48.804-.37 0-.73-.009-1.098-.018l-43.593 31.377c.028.582.046 1.163.046 1.735 0 20.204-16.283 36.636-36.294 36.636-17.566 0-32.263-12.658-35.584-29.412L4.41 164.654c15.223 54.313 64.673 94.132 123.369 94.132 70.818 0 128.221-57.938 128.221-129.393C256 57.93 198.597 0 127.779 0zM80.352 196.332l-15.749-6.568c2.787 5.867 7.621 10.775 14.033 13.47 13.857 5.83 29.836-.803 35.612-14.799a27.555 27.555 0 0 0 .046-21.035c-2.768-6.79-7.999-12.086-14.706-14.909-6.67-2.795-13.811-2.694-20.085-.304l16.275 6.79c10.222 4.3 15.056 16.145 10.794 26.46-4.253 10.314-15.998 15.195-26.22 10.895zm121.957-100.29c0-17.925-14.457-32.52-32.217-32.52-17.769 0-32.226 14.595-32.226 32.52 0 17.926 14.457 32.512 32.226 32.512 17.76 0 32.217-14.586 32.217-32.512zm-56.37-.055c0-13.488 10.84-24.42 24.2-24.42 13.368 0 24.208 10.932 24.208 24.42 0 13.488-10.84 24.421-24.209 24.421-13.359 0-24.2-10.933-24.2-24.42z" fill="#1A1918"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,108 +1,111 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>BUNDLE_ID</key>
|
<key>BUNDLE_ID</key>
|
||||||
<string>dev.solsynth.solian</string>
|
<string>dev.solsynth.solian</string>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Solian</string>
|
<string>Solian</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>solian</string>
|
<string>solian</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Viewer</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLName</key>
|
||||||
<array>
|
<string></string>
|
||||||
<string>solian</string>
|
<key>CFBundleURLSchemes</key>
|
||||||
</array>
|
<array>
|
||||||
</dict>
|
<string>solian</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
</dict>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
</array>
|
||||||
<key>CLIENT_ID</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>CLIENT_ID</key>
|
||||||
<false/>
|
<string>961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig.apps.googleusercontent.com</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<true/>
|
<false />
|
||||||
<key>NSCalendarsUsageDescription</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
<true />
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCalendarsUsageDescription</key>
|
||||||
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
<string>Grant access to Calander help us to shows Solar Calander with your own events.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>Allow the Solar Network verify your ownership of the logged in account and continue your action quickly.</string>
|
<string>Grant access to Camera will allow Solian take photo or video for your post.</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
<string>Allow the Solar Network verify your ownership of the logged in account and continue
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
your action quickly.</string>
|
||||||
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<string>Grant access to Microphone will allow Solian record audio for your post.</string>
|
||||||
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<key>NSUserActivityTypes</key>
|
<string>Grant access to Photo Library will allow Solian download photo to album for you.</string>
|
||||||
<array>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>INStartCallIntent</string>
|
<string>Grant access to Photo Library will allow Solian upload photo or video for your post.</string>
|
||||||
<string>INSendMessageIntent</string>
|
<key>NSUserActivityTypes</key>
|
||||||
</array>
|
<array>
|
||||||
<key>PLIST_VERSION</key>
|
<string>INStartCallIntent</string>
|
||||||
<string>1</string>
|
<string>INSendMessageIntent</string>
|
||||||
<key>REVERSED_CLIENT_ID</key>
|
</array>
|
||||||
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
<key>PLIST_VERSION</key>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<string>1</string>
|
||||||
<true/>
|
<key>REVERSED_CLIENT_ID</key>
|
||||||
<key>UIBackgroundModes</key>
|
<string>com.googleusercontent.apps.961776991058-stt7et4qvn3cpscl4r61gl1hnlatqkig</string>
|
||||||
<array>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<string>fetch</string>
|
<true />
|
||||||
<string>audio</string>
|
<key>UIBackgroundModes</key>
|
||||||
<string>remote-notification</string>
|
<array>
|
||||||
<string>voip</string>
|
<string>fetch</string>
|
||||||
</array>
|
<string>audio</string>
|
||||||
<key>UILaunchStoryboardName</key>
|
<string>remote-notification</string>
|
||||||
<string>LaunchScreen</string>
|
<string>voip</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
</array>
|
||||||
<string>Main</string>
|
<key>UILaunchStoryboardName</key>
|
||||||
<key>UIStatusBarHidden</key>
|
<string>LaunchScreen</string>
|
||||||
<false/>
|
<key>UIMainStoryboardFile</key>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<string>Main</string>
|
||||||
<array>
|
<key>UIStatusBarHidden</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<false />
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<array>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>WKCompanionAppBundleIdentifier</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
</array>
|
||||||
<array>
|
<key>WKCompanionAppBundleIdentifier</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</dict>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -9,6 +9,7 @@ import UserNotifications
|
|||||||
import Intents
|
import Intents
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
import KingfisherWebP
|
||||||
|
|
||||||
enum ParseNotificationPayloadError: Error {
|
enum ParseNotificationPayloadError: Error {
|
||||||
case missingMetadata(String)
|
case missingMetadata(String)
|
||||||
@@ -24,6 +25,11 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
_ request: UNNotificationRequest,
|
_ request: UNNotificationRequest,
|
||||||
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
|
||||||
) {
|
) {
|
||||||
|
KingfisherManager.shared.defaultOptions += [
|
||||||
|
.processor(WebPProcessor.default),
|
||||||
|
.cacheSerializer(WebPSerializer.default)
|
||||||
|
]
|
||||||
|
|
||||||
self.contentHandler = contentHandler
|
self.contentHandler = contentHandler
|
||||||
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
guard let bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
|
||||||
contentHandler(request.content)
|
contentHandler(request.content)
|
||||||
@@ -64,40 +70,12 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown)
|
let handle = INPersonHandle(value: "\(metaCopy["user_id"] ?? "")", type: .unknown)
|
||||||
|
|
||||||
if let pfpUrl = pfpUrl, let url = URL(string: pfpUrl) {
|
let completeNotificationProcessing: (Data?) -> Void = { imageData in
|
||||||
let targetSize = 512
|
|
||||||
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
|
||||||
|
|
||||||
KingfisherManager.shared.retrieveImage(with: url, options: [.processor(scaleProcessor)], completionHandler: { result in
|
|
||||||
var image: Data?
|
|
||||||
switch result {
|
|
||||||
case .success(let value):
|
|
||||||
image = value.image.pngData()
|
|
||||||
case .failure(let error):
|
|
||||||
print("Unable to get pfp url: \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
let sender = INPerson(
|
|
||||||
personHandle: handle,
|
|
||||||
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
|
|
||||||
displayName: content.title,
|
|
||||||
image: image == nil ? nil : INImage(imageData: image!),
|
|
||||||
contactIdentifier: nil,
|
|
||||||
customIdentifier: nil
|
|
||||||
)
|
|
||||||
|
|
||||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
|
||||||
self.donateInteraction(for: intent)
|
|
||||||
|
|
||||||
content.categoryIdentifier = "CHAT_MESSAGE"
|
|
||||||
self.contentHandler?(content)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
let sender = INPerson(
|
let sender = INPerson(
|
||||||
personHandle: handle,
|
personHandle: handle,
|
||||||
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
|
nameComponents: PersonNameComponents(nickname: "\(metaCopy["sender_name"] ?? "")"),
|
||||||
displayName: content.title,
|
displayName: content.title,
|
||||||
image: nil,
|
image: imageData == nil ? nil : INImage(imageData: imageData!),
|
||||||
contactIdentifier: nil,
|
contactIdentifier: nil,
|
||||||
customIdentifier: nil
|
customIdentifier: nil
|
||||||
)
|
)
|
||||||
@@ -105,8 +83,37 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
let intent = self.createMessageIntent(with: sender, meta: metaCopy, body: content.body)
|
||||||
self.donateInteraction(for: intent)
|
self.donateInteraction(for: intent)
|
||||||
|
|
||||||
content.categoryIdentifier = "CHAT_MESSAGE"
|
if let updatedContent = try? request.content.updating(from: intent) {
|
||||||
self.contentHandler?(content)
|
if let mutableContent = updatedContent.mutableCopy() as? UNMutableNotificationContent {
|
||||||
|
mutableContent.categoryIdentifier = "CHAT_MESSAGE"
|
||||||
|
self.contentHandler?(mutableContent)
|
||||||
|
} else {
|
||||||
|
self.contentHandler?(updatedContent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content.categoryIdentifier = "CHAT_MESSAGE"
|
||||||
|
self.contentHandler?(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let pfpUrl = pfpUrl, let url = URL(string: pfpUrl) {
|
||||||
|
let targetSize = 512
|
||||||
|
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: targetSize, height: targetSize), mode: .aspectFit)
|
||||||
|
|
||||||
|
KingfisherManager.shared.retrieveImage(with: url, options: [
|
||||||
|
.processor(scaleProcessor)
|
||||||
|
], completionHandler: { result in
|
||||||
|
var image: Data?
|
||||||
|
switch result {
|
||||||
|
case .success(let value):
|
||||||
|
image = value.image.pngData()
|
||||||
|
case .failure(let error):
|
||||||
|
print("Unable to get pfp url: \(error)")
|
||||||
|
}
|
||||||
|
completeNotificationProcessing(image)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
completeNotificationProcessing(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import 'package:talker_flutter/talker_flutter.dart';
|
|||||||
import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
|
import 'package:talker_riverpod_logger/talker_riverpod_logger.dart';
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
import 'package:protocol_handler/protocol_handler.dart';
|
||||||
|
|
||||||
@pragma('vm:entry-point')
|
@pragma('vm:entry-point')
|
||||||
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
|
||||||
@@ -50,6 +51,12 @@ void main() async {
|
|||||||
GoRouter.optionURLReflectsImperativeAPIs = true;
|
GoRouter.optionURLReflectsImperativeAPIs = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!kIsWeb && (Platform.isLinux || Platform.isMacOS || Platform.isWindows)) {
|
||||||
|
talker.info("[SplashScreen] Initializing desktop window manager...");
|
||||||
|
await protocolHandler.register('myprotocol');
|
||||||
|
talker.info("[SplashScreen] Desktop window manager is ready!");
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ part 'poll.g.dart';
|
|||||||
@freezed
|
@freezed
|
||||||
sealed class SnPollWithStats with _$SnPollWithStats {
|
sealed class SnPollWithStats with _$SnPollWithStats {
|
||||||
const factory SnPollWithStats({
|
const factory SnPollWithStats({
|
||||||
required Map<String, dynamic>? userAnswer,
|
required SnPollAnswer? userAnswer,
|
||||||
@Default({}) Map<String, dynamic> stats,
|
@Default({}) Map<String, dynamic> stats,
|
||||||
required String id,
|
required String id,
|
||||||
required List<SnPollQuestion> questions,
|
required List<SnPollQuestion> questions,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$SnPollWithStats {
|
mixin _$SnPollWithStats {
|
||||||
|
|
||||||
Map<String, dynamic>? get userAnswer; Map<String, dynamic> get stats; String get id; List<SnPollQuestion> get questions; String? get title; String? get description; DateTime? get endedAt; String get publisherId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
SnPollAnswer? get userAnswer; Map<String, dynamic> get stats; String get id; List<SnPollQuestion> get questions; String? get title; String? get description; DateTime? get endedAt; String get publisherId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||||
/// Create a copy of SnPollWithStats
|
/// Create a copy of SnPollWithStats
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -28,12 +28,12 @@ $SnPollWithStatsCopyWith<SnPollWithStats> get copyWith => _$SnPollWithStatsCopyW
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPollWithStats&&const DeepCollectionEquality().equals(other.userAnswer, userAnswer)&&const DeepCollectionEquality().equals(other.stats, stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.questions, questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnPollWithStats&&(identical(other.userAnswer, userAnswer) || other.userAnswer == userAnswer)&&const DeepCollectionEquality().equals(other.stats, stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.questions, questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(userAnswer),const DeepCollectionEquality().hash(stats),id,const DeepCollectionEquality().hash(questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt);
|
int get hashCode => Object.hash(runtimeType,userAnswer,const DeepCollectionEquality().hash(stats),id,const DeepCollectionEquality().hash(questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -48,11 +48,11 @@ abstract mixin class $SnPollWithStatsCopyWith<$Res> {
|
|||||||
factory $SnPollWithStatsCopyWith(SnPollWithStats value, $Res Function(SnPollWithStats) _then) = _$SnPollWithStatsCopyWithImpl;
|
factory $SnPollWithStatsCopyWith(SnPollWithStats value, $Res Function(SnPollWithStats) _then) = _$SnPollWithStatsCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
$SnPollAnswerCopyWith<$Res>? get userAnswer;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -68,7 +68,7 @@ class _$SnPollWithStatsCopyWithImpl<$Res>
|
|||||||
@pragma('vm:prefer-inline') @override $Res call({Object? userAnswer = freezed,Object? stats = null,Object? id = null,Object? questions = null,Object? title = freezed,Object? description = freezed,Object? endedAt = freezed,Object? publisherId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? userAnswer = freezed,Object? stats = null,Object? id = null,Object? questions = null,Object? title = freezed,Object? description = freezed,Object? endedAt = freezed,Object? publisherId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
userAnswer: freezed == userAnswer ? _self.userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
userAnswer: freezed == userAnswer ? _self.userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>?,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable
|
as SnPollAnswer?,stats: null == stats ? _self.stats : stats // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
as Map<String, dynamic>,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,questions: null == questions ? _self.questions : questions // ignore: cast_nullable_to_non_nullable
|
as String,questions: null == questions ? _self.questions : questions // ignore: cast_nullable_to_non_nullable
|
||||||
as List<SnPollQuestion>,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
as List<SnPollQuestion>,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -81,7 +81,19 @@ as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ign
|
|||||||
as DateTime?,
|
as DateTime?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
/// Create a copy of SnPollWithStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPollAnswerCopyWith<$Res>? get userAnswer {
|
||||||
|
if (_self.userAnswer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnPollAnswerCopyWith<$Res>(_self.userAnswer!, (value) {
|
||||||
|
return _then(_self.copyWith(userAnswer: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -160,7 +172,7 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnPollWithStats() when $default != null:
|
case _SnPollWithStats() when $default != null:
|
||||||
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
@@ -181,7 +193,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnPollWithStats():
|
case _SnPollWithStats():
|
||||||
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||||
@@ -198,7 +210,7 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _SnPollWithStats() when $default != null:
|
case _SnPollWithStats() when $default != null:
|
||||||
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.title,_that.description,_that.endedAt,_that.publisherId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||||
@@ -213,18 +225,10 @@ return $default(_that.userAnswer,_that.stats,_that.id,_that.questions,_that.titl
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
|
|
||||||
class _SnPollWithStats implements SnPollWithStats {
|
class _SnPollWithStats implements SnPollWithStats {
|
||||||
const _SnPollWithStats({required final Map<String, dynamic>? userAnswer, final Map<String, dynamic> stats = const {}, required this.id, required final List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _userAnswer = userAnswer,_stats = stats,_questions = questions;
|
const _SnPollWithStats({required this.userAnswer, final Map<String, dynamic> stats = const {}, required this.id, required final List<SnPollQuestion> questions, this.title, this.description, this.endedAt, required this.publisherId, required this.createdAt, required this.updatedAt, this.deletedAt}): _stats = stats,_questions = questions;
|
||||||
factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
|
factory _SnPollWithStats.fromJson(Map<String, dynamic> json) => _$SnPollWithStatsFromJson(json);
|
||||||
|
|
||||||
final Map<String, dynamic>? _userAnswer;
|
@override final SnPollAnswer? userAnswer;
|
||||||
@override Map<String, dynamic>? get userAnswer {
|
|
||||||
final value = _userAnswer;
|
|
||||||
if (value == null) return null;
|
|
||||||
if (_userAnswer is EqualUnmodifiableMapView) return _userAnswer;
|
|
||||||
// ignore: implicit_dynamic_type
|
|
||||||
return EqualUnmodifiableMapView(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Map<String, dynamic> _stats;
|
final Map<String, dynamic> _stats;
|
||||||
@override@JsonKey() Map<String, dynamic> get stats {
|
@override@JsonKey() Map<String, dynamic> get stats {
|
||||||
if (_stats is EqualUnmodifiableMapView) return _stats;
|
if (_stats is EqualUnmodifiableMapView) return _stats;
|
||||||
@@ -261,12 +265,12 @@ Map<String, dynamic> toJson() {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPollWithStats&&const DeepCollectionEquality().equals(other._userAnswer, _userAnswer)&&const DeepCollectionEquality().equals(other._stats, _stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._questions, _questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnPollWithStats&&(identical(other.userAnswer, userAnswer) || other.userAnswer == userAnswer)&&const DeepCollectionEquality().equals(other._stats, _stats)&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other._questions, _questions)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.endedAt, endedAt) || other.endedAt == endedAt)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_userAnswer),const DeepCollectionEquality().hash(_stats),id,const DeepCollectionEquality().hash(_questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt);
|
int get hashCode => Object.hash(runtimeType,userAnswer,const DeepCollectionEquality().hash(_stats),id,const DeepCollectionEquality().hash(_questions),title,description,endedAt,publisherId,createdAt,updatedAt,deletedAt);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
@@ -281,11 +285,11 @@ abstract mixin class _$SnPollWithStatsCopyWith<$Res> implements $SnPollWithStats
|
|||||||
factory _$SnPollWithStatsCopyWith(_SnPollWithStats value, $Res Function(_SnPollWithStats) _then) = __$SnPollWithStatsCopyWithImpl;
|
factory _$SnPollWithStatsCopyWith(_SnPollWithStats value, $Res Function(_SnPollWithStats) _then) = __$SnPollWithStatsCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
Map<String, dynamic>? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
SnPollAnswer? userAnswer, Map<String, dynamic> stats, String id, List<SnPollQuestion> questions, String? title, String? description, DateTime? endedAt, String publisherId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@override $SnPollAnswerCopyWith<$Res>? get userAnswer;
|
||||||
|
|
||||||
}
|
}
|
||||||
/// @nodoc
|
/// @nodoc
|
||||||
@@ -300,8 +304,8 @@ class __$SnPollWithStatsCopyWithImpl<$Res>
|
|||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@override @pragma('vm:prefer-inline') $Res call({Object? userAnswer = freezed,Object? stats = null,Object? id = null,Object? questions = null,Object? title = freezed,Object? description = freezed,Object? endedAt = freezed,Object? publisherId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? userAnswer = freezed,Object? stats = null,Object? id = null,Object? questions = null,Object? title = freezed,Object? description = freezed,Object? endedAt = freezed,Object? publisherId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||||
return _then(_SnPollWithStats(
|
return _then(_SnPollWithStats(
|
||||||
userAnswer: freezed == userAnswer ? _self._userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
userAnswer: freezed == userAnswer ? _self.userAnswer : userAnswer // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>?,stats: null == stats ? _self._stats : stats // ignore: cast_nullable_to_non_nullable
|
as SnPollAnswer?,stats: null == stats ? _self._stats : stats // ignore: cast_nullable_to_non_nullable
|
||||||
as Map<String, dynamic>,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
as Map<String, dynamic>,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||||
as String,questions: null == questions ? _self._questions : questions // ignore: cast_nullable_to_non_nullable
|
as String,questions: null == questions ? _self._questions : questions // ignore: cast_nullable_to_non_nullable
|
||||||
as List<SnPollQuestion>,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
as List<SnPollQuestion>,title: freezed == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -315,7 +319,19 @@ as DateTime?,
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a copy of SnPollWithStats
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$SnPollAnswerCopyWith<$Res>? get userAnswer {
|
||||||
|
if (_self.userAnswer == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $SnPollAnswerCopyWith<$Res>(_self.userAnswer!, (value) {
|
||||||
|
return _then(_self.copyWith(userAnswer: value));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ part of 'poll.dart';
|
|||||||
|
|
||||||
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
_SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
||||||
_SnPollWithStats(
|
_SnPollWithStats(
|
||||||
userAnswer: json['user_answer'] as Map<String, dynamic>?,
|
userAnswer:
|
||||||
|
json['user_answer'] == null
|
||||||
|
? null
|
||||||
|
: SnPollAnswer.fromJson(
|
||||||
|
json['user_answer'] as Map<String, dynamic>,
|
||||||
|
),
|
||||||
stats: json['stats'] as Map<String, dynamic>? ?? const {},
|
stats: json['stats'] as Map<String, dynamic>? ?? const {},
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
questions:
|
questions:
|
||||||
@@ -32,7 +37,7 @@ _SnPollWithStats _$SnPollWithStatsFromJson(Map<String, dynamic> json) =>
|
|||||||
|
|
||||||
Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
|
Map<String, dynamic> _$SnPollWithStatsToJson(_SnPollWithStats instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'user_answer': instance.userAnswer,
|
'user_answer': instance.userAnswer?.toJson(),
|
||||||
'stats': instance.stats,
|
'stats': instance.stats,
|
||||||
'id': instance.id,
|
'id': instance.id,
|
||||||
'questions': instance.questions.map((e) => e.toJson()).toList(),
|
'questions': instance.questions.map((e) => e.toJson()).toList(),
|
||||||
|
|||||||
@@ -120,9 +120,11 @@ class ActivityRpcServer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Set up IPC close handler
|
// Set up IPC close handler
|
||||||
_ipcServer!.onSocketClose = (socket) {
|
if (!kIsWeb) {
|
||||||
handlers['close']?.call(socket);
|
(_ipcServer as dynamic).onSocketClose = (socket) {
|
||||||
};
|
handlers['close']?.call(socket);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
await _ipcServer!.start();
|
await _ipcServer!.start();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'activity_rpc.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$presenceActivitiesHash() =>
|
String _$presenceActivitiesHash() =>
|
||||||
r'dcea3cad01b4010c0087f5281413d83a754c2a17';
|
r'3bfaa638eeb961ecd62a32d6a7760a6a7e7bf6f2';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:island/screens/about.dart';
|
import 'package:island/screens/about.dart';
|
||||||
import 'package:island/screens/developers/app_detail.dart';
|
import 'package:island/screens/developers/app_detail.dart';
|
||||||
import 'package:island/screens/developers/bot_detail.dart';
|
import 'package:island/screens/developers/bot_detail.dart';
|
||||||
import 'package:island/screens/developers/edit_app.dart';
|
|
||||||
import 'package:island/screens/developers/edit_bot.dart';
|
|
||||||
import 'package:island/screens/developers/hub.dart';
|
import 'package:island/screens/developers/hub.dart';
|
||||||
import 'package:island/screens/developers/new_app.dart';
|
|
||||||
import 'package:island/screens/developers/new_bot.dart';
|
|
||||||
import 'package:island/screens/developers/edit_project.dart';
|
import 'package:island/screens/developers/edit_project.dart';
|
||||||
import 'package:island/screens/developers/new_project.dart';
|
import 'package:island/screens/developers/new_project.dart';
|
||||||
import 'package:island/screens/discovery/articles.dart';
|
import 'package:island/screens/discovery/articles.dart';
|
||||||
@@ -570,25 +566,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return const SizedBox.shrink(); // Temporary placeholder
|
return const SizedBox.shrink(); // Temporary placeholder
|
||||||
},
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
|
||||||
name: 'developerAppNew',
|
|
||||||
path: 'apps/new',
|
|
||||||
builder:
|
|
||||||
(context, state) => NewCustomAppScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'developerAppEdit',
|
|
||||||
path: 'apps/:id/edit',
|
|
||||||
builder:
|
|
||||||
(context, state) => EditAppScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
id: state.pathParameters['id']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'developerAppDetail',
|
name: 'developerAppDetail',
|
||||||
path: 'apps/:appId',
|
path: 'apps/:appId',
|
||||||
@@ -599,15 +576,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
appId: state.pathParameters['appId']!,
|
appId: state.pathParameters['appId']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'developerBotNew',
|
|
||||||
path: 'bots/new',
|
|
||||||
builder:
|
|
||||||
(context, state) => NewBotScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'developerBotDetail',
|
name: 'developerBotDetail',
|
||||||
path: 'bots/:botId',
|
path: 'bots/:botId',
|
||||||
@@ -618,16 +586,6 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
botId: state.pathParameters['botId']!,
|
botId: state.pathParameters['botId']!,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
|
||||||
name: 'developerBotEdit',
|
|
||||||
path: 'bots/:id/edit',
|
|
||||||
builder:
|
|
||||||
(context, state) => EditBotScreen(
|
|
||||||
publisherName: state.pathParameters['name']!,
|
|
||||||
projectId: state.pathParameters['projectId']!,
|
|
||||||
id: state.pathParameters['id']!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Widget getProviderIcon(String provider, {double size = 24, Color? color}) {
|
|||||||
case 'github':
|
case 'github':
|
||||||
case 'discord':
|
case 'discord':
|
||||||
case 'afdian':
|
case 'afdian':
|
||||||
|
case 'steam':
|
||||||
return SvgPicture.asset(
|
return SvgPicture.asset(
|
||||||
'assets/images/oidc/$providerLower.svg',
|
'assets/images/oidc/$providerLower.svg',
|
||||||
width: size,
|
width: size,
|
||||||
@@ -64,6 +65,8 @@ String getLocalizedProviderName(String provider) {
|
|||||||
return 'accountConnectionProviderAfdian'.tr();
|
return 'accountConnectionProviderAfdian'.tr();
|
||||||
case 'spotify':
|
case 'spotify':
|
||||||
return 'accountConnectionProviderSpotify'.tr();
|
return 'accountConnectionProviderSpotify'.tr();
|
||||||
|
case 'steam':
|
||||||
|
return 'accountConnectionProviderSteam'.tr();
|
||||||
default:
|
default:
|
||||||
return provider;
|
return provider;
|
||||||
}
|
}
|
||||||
@@ -164,6 +167,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
|||||||
'discord',
|
'discord',
|
||||||
'afdian',
|
'afdian',
|
||||||
'spotify',
|
'spotify',
|
||||||
|
'steam',
|
||||||
];
|
];
|
||||||
|
|
||||||
Future<void> addConnection() async {
|
Future<void> addConnection() async {
|
||||||
@@ -199,12 +203,7 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
|||||||
} finally {
|
} finally {
|
||||||
if (context.mounted) hideLoadingModal(context);
|
if (context.mounted) hideLoadingModal(context);
|
||||||
}
|
}
|
||||||
case 'microsoft':
|
default:
|
||||||
case 'google':
|
|
||||||
case 'github':
|
|
||||||
case 'discord':
|
|
||||||
case 'afdian':
|
|
||||||
case 'spotify':
|
|
||||||
final serverUrl = ref.watch(serverUrlProvider);
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
final accessToken = ref.watch(tokenProvider);
|
final accessToken = ref.watch(tokenProvider);
|
||||||
launchUrlString(
|
launchUrlString(
|
||||||
@@ -212,9 +211,6 @@ class AccountConnectionNewSheet extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
if (context.mounted) Navigator.pop(context, true);
|
if (context.mounted) Navigator.pop(context, true);
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
showSnackBar('accountConnectionAddError'.tr());
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "dart:async";
|
import "dart:async";
|
||||||
|
import "dart:math" as math;
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:file_picker/file_picker.dart";
|
import "package:file_picker/file_picker.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
@@ -140,6 +141,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
final messageController = useTextEditingController();
|
final messageController = useTextEditingController();
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
|
|
||||||
|
// Scroll animation notifiers
|
||||||
|
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||||
|
|
||||||
final messageReplyingTo = useState<SnChatMessage?>(null);
|
final messageReplyingTo = useState<SnChatMessage?>(null);
|
||||||
final messageForwardingTo = useState<SnChatMessage?>(null);
|
final messageForwardingTo = useState<SnChatMessage?>(null);
|
||||||
final messageEditingTo = useState<SnChatMessage?>(null);
|
final messageEditingTo = useState<SnChatMessage?>(null);
|
||||||
@@ -164,6 +168,12 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
isLoading = true;
|
isLoading = true;
|
||||||
messagesNotifier.loadMore().then((_) => isLoading = false);
|
messagesNotifier.loadMore().then((_) => isLoading = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update gradient animations
|
||||||
|
final pixels = scrollController.position.pixels;
|
||||||
|
|
||||||
|
// Bottom gradient: appears when not at bottom (pixels > 0)
|
||||||
|
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollController.addListener(onScroll);
|
scrollController.addListener(onScroll);
|
||||||
@@ -589,7 +599,9 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
listController: listController,
|
listController: listController,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
top: 16,
|
top: 16,
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
bottom:
|
||||||
|
MediaQuery.of(context).padding.bottom +
|
||||||
|
80, // Leave space for chat input
|
||||||
),
|
),
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
reverse: true, // Show newest messages at the bottom
|
reverse: true, // Show newest messages at the bottom
|
||||||
@@ -828,7 +840,7 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Messages and Input in Column
|
// Messages only in Column
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -872,73 +884,6 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (!isSelectionMode.value)
|
|
||||||
chatRoom.when(
|
|
||||||
data:
|
|
||||||
(room) => Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
ChatInput(
|
|
||||||
messageController: messageController,
|
|
||||||
chatRoom: room!,
|
|
||||||
onSend: sendMessage,
|
|
||||||
onClear: () {
|
|
||||||
if (messageEditingTo.value != null) {
|
|
||||||
attachments.value.clear();
|
|
||||||
messageController.clear();
|
|
||||||
}
|
|
||||||
messageEditingTo.value = null;
|
|
||||||
messageReplyingTo.value = null;
|
|
||||||
messageForwardingTo.value = null;
|
|
||||||
},
|
|
||||||
messageEditingTo: messageEditingTo.value,
|
|
||||||
messageReplyingTo: messageReplyingTo.value,
|
|
||||||
messageForwardingTo: messageForwardingTo.value,
|
|
||||||
onPickFile: (bool isPhoto) {
|
|
||||||
if (isPhoto) {
|
|
||||||
pickPhotoMedia();
|
|
||||||
} else {
|
|
||||||
pickVideoMedia();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onPickAudio: pickAudioMedia,
|
|
||||||
onPickGeneralFile: pickGeneralFile,
|
|
||||||
onLinkAttachment: linkAttachment,
|
|
||||||
attachments: attachments.value,
|
|
||||||
onUploadAttachment: uploadAttachment,
|
|
||||||
onDeleteAttachment: (index) async {
|
|
||||||
final attachment = attachments.value[index];
|
|
||||||
if (attachment.isOnCloud &&
|
|
||||||
!attachment.isLink) {
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
await client.delete(
|
|
||||||
'/drive/files/${attachment.data.id}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
final clone = List.of(attachments.value);
|
|
||||||
clone.removeAt(index);
|
|
||||||
attachments.value = clone;
|
|
||||||
},
|
|
||||||
onMoveAttachment: (idx, delta) {
|
|
||||||
if (idx + delta < 0 ||
|
|
||||||
idx + delta >= attachments.value.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final clone = List.of(attachments.value);
|
|
||||||
clone.insert(idx + delta, clone.removeAt(idx));
|
|
||||||
attachments.value = clone;
|
|
||||||
},
|
|
||||||
onAttachmentsChanged: (newAttachments) {
|
|
||||||
attachments.value = newAttachments;
|
|
||||||
},
|
|
||||||
attachmentProgress: attachmentProgress.value,
|
|
||||||
),
|
|
||||||
Gap(MediaQuery.of(context).padding.bottom),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
error: (_, _) => const SizedBox.shrink(),
|
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -977,6 +922,112 @@ class ChatRoomScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Bottom gradient - appears when scrolling towards newer messages (behind chat input)
|
||||||
|
if (!isSelectionMode.value)
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: bottomGradientNotifier.value,
|
||||||
|
builder:
|
||||||
|
(context, child) => Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: bottomGradientNotifier.value.value,
|
||||||
|
child: Container(
|
||||||
|
height: math.min(
|
||||||
|
MediaQuery.of(context).size.height * 0.1,
|
||||||
|
128,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainer.withOpacity(0.8),
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainer.withOpacity(0.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Chat Input positioned above gradient (higher z-index)
|
||||||
|
if (!isSelectionMode.value)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0, // At the very bottom, above gradient
|
||||||
|
child: chatRoom.when(
|
||||||
|
data:
|
||||||
|
(room) => Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
ChatInput(
|
||||||
|
messageController: messageController,
|
||||||
|
chatRoom: room!,
|
||||||
|
onSend: sendMessage,
|
||||||
|
onClear: () {
|
||||||
|
if (messageEditingTo.value != null) {
|
||||||
|
attachments.value.clear();
|
||||||
|
messageController.clear();
|
||||||
|
}
|
||||||
|
messageEditingTo.value = null;
|
||||||
|
messageReplyingTo.value = null;
|
||||||
|
messageForwardingTo.value = null;
|
||||||
|
},
|
||||||
|
messageEditingTo: messageEditingTo.value,
|
||||||
|
messageReplyingTo: messageReplyingTo.value,
|
||||||
|
messageForwardingTo: messageForwardingTo.value,
|
||||||
|
onPickFile: (bool isPhoto) {
|
||||||
|
if (isPhoto) {
|
||||||
|
pickPhotoMedia();
|
||||||
|
} else {
|
||||||
|
pickVideoMedia();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPickAudio: pickAudioMedia,
|
||||||
|
onPickGeneralFile: pickGeneralFile,
|
||||||
|
onLinkAttachment: linkAttachment,
|
||||||
|
attachments: attachments.value,
|
||||||
|
onUploadAttachment: uploadAttachment,
|
||||||
|
onDeleteAttachment: (index) async {
|
||||||
|
final attachment = attachments.value[index];
|
||||||
|
if (attachment.isOnCloud && !attachment.isLink) {
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
await client.delete(
|
||||||
|
'/drive/files/${attachment.data.id}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final clone = List.of(attachments.value);
|
||||||
|
clone.removeAt(index);
|
||||||
|
attachments.value = clone;
|
||||||
|
},
|
||||||
|
onMoveAttachment: (idx, delta) {
|
||||||
|
if (idx + delta < 0 ||
|
||||||
|
idx + delta >= attachments.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final clone = List.of(attachments.value);
|
||||||
|
clone.insert(idx + delta, clone.removeAt(idx));
|
||||||
|
attachments.value = clone;
|
||||||
|
},
|
||||||
|
onAttachmentsChanged: (newAttachments) {
|
||||||
|
attachments.value = newAttachments;
|
||||||
|
},
|
||||||
|
attachmentProgress: attachmentProgress.value,
|
||||||
|
),
|
||||||
|
Gap(MediaQuery.of(context).padding.bottom),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
error: (_, _) => const SizedBox.shrink(),
|
||||||
|
loading: () => const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
// Selection mode toolbar
|
// Selection mode toolbar
|
||||||
if (isSelectionMode.value)
|
if (isSelectionMode.value)
|
||||||
Positioned(
|
Positioned(
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/custom_app.dart';
|
import 'package:island/models/custom_app.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/developers/edit_app.dart';
|
||||||
|
import 'package:island/screens/developers/new_app.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -68,12 +73,18 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'developerAppNew',
|
context: context,
|
||||||
pathParameters: {
|
isScrollControlled: true,
|
||||||
'name': publisherName,
|
builder:
|
||||||
'projectId': projectId,
|
(context) => SheetScaffold(
|
||||||
},
|
titleText: 'createCustomApp'.tr(),
|
||||||
|
child: NewCustomAppScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
isModal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
@@ -83,129 +94,171 @@ class CustomAppsScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return RefreshIndicator(
|
return ExtendedRefreshIndicator(
|
||||||
onRefresh:
|
onRefresh:
|
||||||
() => ref.refresh(
|
() => ref.refresh(
|
||||||
customAppsProvider(publisherName, projectId).future,
|
customAppsProvider(publisherName, projectId).future,
|
||||||
),
|
),
|
||||||
child: ListView.builder(
|
child: Column(
|
||||||
padding: EdgeInsets.only(top: 4),
|
children: [
|
||||||
itemCount: data.length,
|
const Gap(8),
|
||||||
itemBuilder: (context, index) {
|
Card(
|
||||||
final app = data[index];
|
child: ListTile(
|
||||||
return Card(
|
title: Text('customApps').tr().padding(horizontal: 8),
|
||||||
margin: const EdgeInsets.all(8.0),
|
trailing: IconButton(
|
||||||
clipBehavior: Clip.antiAlias,
|
onPressed: () {
|
||||||
child: InkWell(
|
showModalBottomSheet(
|
||||||
onTap: () {
|
context: context,
|
||||||
context.pushNamed(
|
isScrollControlled: true,
|
||||||
'developerAppDetail',
|
builder:
|
||||||
pathParameters: {
|
(context) => SheetScaffold(
|
||||||
'name': publisherName,
|
titleText: 'createCustomApp'.tr(),
|
||||||
'projectId': projectId,
|
child: NewCustomAppScreen(
|
||||||
'appId': app.id,
|
publisherName: publisherName,
|
||||||
},
|
projectId: projectId,
|
||||||
);
|
isModal: true,
|
||||||
},
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
height: 150,
|
|
||||||
child: Stack(
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
if (app.background != null)
|
|
||||||
CloudFileWidget(
|
|
||||||
item: app.background!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
).clipRRect(topLeft: 8, topRight: 8),
|
|
||||||
if (app.picture != null)
|
|
||||||
Positioned(
|
|
||||||
left: 16,
|
|
||||||
bottom: 16,
|
|
||||||
child: ProfilePictureWidget(
|
|
||||||
fileId: app.picture!.id,
|
|
||||||
radius: 40,
|
|
||||||
fallbackIcon: Symbols.apps,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: data.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final app = data[index];
|
||||||
|
return Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed(
|
||||||
|
'developerAppDetail',
|
||||||
|
pathParameters: {
|
||||||
|
'name': publisherName,
|
||||||
|
'projectId': projectId,
|
||||||
|
'appId': app.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
height: 150,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
if (app.background != null)
|
||||||
|
CloudFileWidget(
|
||||||
|
item: app.background!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
).clipRRect(topLeft: 8, topRight: 8),
|
||||||
|
if (app.picture != null)
|
||||||
|
Positioned(
|
||||||
|
left: 16,
|
||||||
|
bottom: 16,
|
||||||
|
child: ProfilePictureWidget(
|
||||||
|
fileId: app.picture!.id,
|
||||||
|
radius: 40,
|
||||||
|
fallbackIcon: Symbols.apps,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(app.name),
|
||||||
|
subtitle: Text(
|
||||||
|
app.slug,
|
||||||
|
style: GoogleFonts.robotoMono(fontSize: 12),
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.only(
|
||||||
|
left: 20,
|
||||||
|
right: 12,
|
||||||
|
),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text('edit').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Symbols.delete,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'delete',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'edit') {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'editCustomApp'.tr(),
|
||||||
|
child: EditAppScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
id: app.id,
|
||||||
|
isModal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (value == 'delete') {
|
||||||
|
showConfirmAlert(
|
||||||
|
'deleteCustomAppHint'.tr(),
|
||||||
|
'deleteCustomApp'.tr(),
|
||||||
|
).then((confirm) {
|
||||||
|
if (confirm) {
|
||||||
|
final client = ref.read(
|
||||||
|
apiClientProvider,
|
||||||
|
);
|
||||||
|
client.delete(
|
||||||
|
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
|
||||||
|
);
|
||||||
|
ref.invalidate(
|
||||||
|
customAppsProvider(
|
||||||
|
publisherName,
|
||||||
|
projectId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
);
|
||||||
title: Text(app.name),
|
},
|
||||||
subtitle: Text(
|
|
||||||
app.slug,
|
|
||||||
style: GoogleFonts.robotoMono(fontSize: 12),
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.only(left: 20, right: 12),
|
|
||||||
trailing: PopupMenuButton(
|
|
||||||
itemBuilder:
|
|
||||||
(context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'edit',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.edit),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text('edit').tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'delete',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Symbols.delete,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(
|
|
||||||
'delete',
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
).tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onSelected: (value) {
|
|
||||||
if (value == 'edit') {
|
|
||||||
context.pushNamed(
|
|
||||||
'developerAppEdit',
|
|
||||||
pathParameters: {
|
|
||||||
'name': publisherName,
|
|
||||||
'projectId': projectId,
|
|
||||||
'id': app.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (value == 'delete') {
|
|
||||||
showConfirmAlert(
|
|
||||||
'deleteCustomAppHint'.tr(),
|
|
||||||
'deleteCustomApp'.tr(),
|
|
||||||
).then((confirm) {
|
|
||||||
if (confirm) {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
client.delete(
|
|
||||||
'/develop/developers/$publisherName/projects/$projectId/apps/${app.id}',
|
|
||||||
);
|
|
||||||
ref.invalidate(
|
|
||||||
customAppsProvider(
|
|
||||||
publisherName,
|
|
||||||
projectId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/bot.dart';
|
import 'package:island/models/bot.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/developers/edit_bot.dart';
|
||||||
|
import 'package:island/screens/developers/new_bot.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
|
import 'package:island/widgets/content/sheet.dart';
|
||||||
import 'package:island/widgets/response.dart';
|
import 'package:island/widgets/response.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:island/widgets/extended_refresh_indicator.dart';
|
import 'package:island/widgets/extended_refresh_indicator.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
part 'bots.g.dart';
|
part 'bots.g.dart';
|
||||||
|
|
||||||
@@ -46,12 +51,18 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.pushNamed(
|
showModalBottomSheet(
|
||||||
'developerBotNew',
|
context: context,
|
||||||
pathParameters: {
|
isScrollControlled: true,
|
||||||
'name': publisherName,
|
builder:
|
||||||
'projectId': projectId,
|
(context) => SheetScaffold(
|
||||||
},
|
titleText: 'createBot'.tr(),
|
||||||
|
child: NewBotScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
isModal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(Symbols.add),
|
icon: const Icon(Symbols.add),
|
||||||
@@ -64,95 +75,133 @@ class BotsScreen extends HookConsumerWidget {
|
|||||||
return ExtendedRefreshIndicator(
|
return ExtendedRefreshIndicator(
|
||||||
onRefresh:
|
onRefresh:
|
||||||
() => ref.refresh(botsProvider(publisherName, projectId).future),
|
() => ref.refresh(botsProvider(publisherName, projectId).future),
|
||||||
child: ListView.builder(
|
child: Column(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
children: [
|
||||||
itemCount: data.length,
|
const Gap(8),
|
||||||
itemBuilder: (context, index) {
|
Card(
|
||||||
final bot = data[index];
|
|
||||||
return Card(
|
|
||||||
margin: const EdgeInsets.all(8.0),
|
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
shape: const RoundedRectangleBorder(
|
title: Text('bots').tr().padding(horizontal: 8),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
trailing: IconButton(
|
||||||
),
|
onPressed: () {
|
||||||
leading: CircleAvatar(
|
showModalBottomSheet(
|
||||||
child:
|
context: context,
|
||||||
bot.account.profile.picture != null
|
isScrollControlled: true,
|
||||||
? ProfilePictureWidget(
|
builder:
|
||||||
file: bot.account.profile.picture!,
|
(context) => SheetScaffold(
|
||||||
)
|
titleText: 'createBot'.tr(),
|
||||||
: const Icon(Symbols.smart_toy),
|
child: NewBotScreen(
|
||||||
),
|
publisherName: publisherName,
|
||||||
title: Text(bot.account.nick),
|
projectId: projectId,
|
||||||
subtitle: Text(bot.account.name),
|
isModal: true,
|
||||||
trailing: PopupMenuButton(
|
),
|
||||||
itemBuilder:
|
|
||||||
(context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
value: 'edit',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.edit),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text('edit').tr(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
PopupMenuItem(
|
|
||||||
value: 'delete',
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Symbols.delete, color: Colors.red),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Text(
|
|
||||||
'delete',
|
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
).tr(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
onSelected: (value) {
|
|
||||||
if (value == 'edit') {
|
|
||||||
context.pushNamed(
|
|
||||||
'developerBotEdit',
|
|
||||||
pathParameters: {
|
|
||||||
'name': publisherName,
|
|
||||||
'projectId': projectId,
|
|
||||||
'id': bot.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else if (value == 'delete') {
|
|
||||||
showConfirmAlert(
|
|
||||||
'deleteBotHint'.tr(),
|
|
||||||
'deleteBot'.tr(),
|
|
||||||
).then((confirm) {
|
|
||||||
if (confirm) {
|
|
||||||
final client = ref.read(apiClientProvider);
|
|
||||||
client.delete(
|
|
||||||
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
|
|
||||||
);
|
|
||||||
ref.invalidate(
|
|
||||||
botsProvider(publisherName, projectId),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
icon: const Icon(Symbols.add),
|
||||||
),
|
),
|
||||||
onTap: () {
|
),
|
||||||
context.pushNamed(
|
),
|
||||||
'developerBotDetail',
|
Expanded(
|
||||||
pathParameters: {
|
child: ListView.builder(
|
||||||
'name': publisherName,
|
padding: EdgeInsets.zero,
|
||||||
'projectId': projectId,
|
itemCount: data.length,
|
||||||
'botId': bot.id,
|
itemBuilder: (context, index) {
|
||||||
},
|
final bot = data[index];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(8.0)),
|
||||||
|
),
|
||||||
|
leading: CircleAvatar(
|
||||||
|
child:
|
||||||
|
bot.account.profile.picture != null
|
||||||
|
? ProfilePictureWidget(
|
||||||
|
file: bot.account.profile.picture!,
|
||||||
|
)
|
||||||
|
: const Icon(Symbols.smart_toy),
|
||||||
|
),
|
||||||
|
title: Text(bot.account.nick),
|
||||||
|
subtitle: Text(bot.account.name),
|
||||||
|
trailing: PopupMenuButton(
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Symbols.edit),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text('edit').tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Symbols.delete,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
'delete',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
).tr(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'edit') {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder:
|
||||||
|
(context) => SheetScaffold(
|
||||||
|
titleText: 'editBot'.tr(),
|
||||||
|
child: EditBotScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
id: bot.id,
|
||||||
|
isModal: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (value == 'delete') {
|
||||||
|
showConfirmAlert(
|
||||||
|
'deleteBotHint'.tr(),
|
||||||
|
'deleteBot'.tr(),
|
||||||
|
).then((confirm) {
|
||||||
|
if (confirm) {
|
||||||
|
final client = ref.read(apiClientProvider);
|
||||||
|
client.delete(
|
||||||
|
'/develop/developers/$publisherName/projects/$projectId/bots/${bot.id}',
|
||||||
|
);
|
||||||
|
ref.invalidate(
|
||||||
|
botsProvider(publisherName, projectId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
context.pushNamed(
|
||||||
|
'developerBotDetail',
|
||||||
|
pathParameters: {
|
||||||
|
'name': publisherName,
|
||||||
|
'projectId': projectId,
|
||||||
|
'botId': bot.id,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -39,11 +39,13 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
final String publisherName;
|
final String publisherName;
|
||||||
final String projectId;
|
final String projectId;
|
||||||
final String? id;
|
final String? id;
|
||||||
|
final bool isModal;
|
||||||
const EditAppScreen({
|
const EditAppScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.publisherName,
|
required this.publisherName,
|
||||||
required this.projectId,
|
required this.projectId,
|
||||||
this.id,
|
this.id,
|
||||||
|
this.isModal = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -177,7 +179,12 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: scopeController,
|
controller: scopeController,
|
||||||
decoration: InputDecoration(labelText: 'scopeName'.tr()),
|
decoration: InputDecoration(
|
||||||
|
labelText: 'scopeName'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
FilledButton.tonalIcon(
|
FilledButton.tonalIcon(
|
||||||
@@ -220,6 +227,9 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
hintText: 'https://example.com/auth/callback',
|
hintText: 'https://example.com/auth/callback',
|
||||||
helperText: 'redirectUriHint'.tr(),
|
helperText: 'redirectUriHint'.tr(),
|
||||||
helperMaxLines: 3,
|
helperMaxLines: 3,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
keyboardType: TextInputType.url,
|
keyboardType: TextInputType.url,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
@@ -316,270 +326,298 @@ class EditAppScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final bodyContent =
|
||||||
|
app == null && !isNew
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: app?.hasError == true && !isNew
|
||||||
|
? ResponseErrorWidget(
|
||||||
|
error: app!.error,
|
||||||
|
onRetry:
|
||||||
|
() => ref.invalidate(
|
||||||
|
customAppProvider(publisherName, projectId, id!),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
child: Container(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHigh,
|
||||||
|
child:
|
||||||
|
background.value != null
|
||||||
|
? CloudFileWidget(
|
||||||
|
item: background.value!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setPicture('background');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 20,
|
||||||
|
bottom: -32,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: ProfilePictureWidget(
|
||||||
|
fileId: picture.value?.id,
|
||||||
|
radius: 40,
|
||||||
|
fallbackIcon: Symbols.apps,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setPicture('picture');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).padding(bottom: 32),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'name'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside:
|
||||||
|
(_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: slugController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'slug'.tr(),
|
||||||
|
helperText: 'slugHint'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTapOutside:
|
||||||
|
(_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: descriptionController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'description'.tr(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
onTapOutside:
|
||||||
|
(_) =>
|
||||||
|
FocusManager.instance.primaryFocus?.unfocus(),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ExpansionPanelList(
|
||||||
|
expansionCallback: (index, isExpanded) {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
enableLinks.value = isExpanded;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
oauthEnabled.value = isExpanded;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
ExpansionPanel(
|
||||||
|
headerBuilder:
|
||||||
|
(context, isExpanded) =>
|
||||||
|
ListTile(title: Text('appLinks').tr()),
|
||||||
|
body: Column(
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: homePageController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'homePageUrl'.tr(),
|
||||||
|
hintText: 'https://example.com',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: privacyPolicyController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'privacyPolicyUrl'.tr(),
|
||||||
|
hintText: 'https://example.com/privacy',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: termsController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'termsOfServiceUrl'.tr(),
|
||||||
|
hintText: 'https://example.com/terms',
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.url,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16, bottom: 24),
|
||||||
|
isExpanded: enableLinks.value,
|
||||||
|
),
|
||||||
|
ExpansionPanel(
|
||||||
|
headerBuilder:
|
||||||
|
(context, isExpanded) =>
|
||||||
|
ListTile(title: Text('oauthConfig').tr()),
|
||||||
|
body: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text('redirectUris'.tr()),
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
...redirectUris.value.map(
|
||||||
|
(uri) => ListTile(
|
||||||
|
title: Text(uri),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
onPressed: () {
|
||||||
|
redirectUris.value =
|
||||||
|
redirectUris.value
|
||||||
|
.where((u) => u != uri)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (redirectUris.value.isNotEmpty)
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.add),
|
||||||
|
title: Text('addRedirectUri'.tr()),
|
||||||
|
onTap: showAddRedirectUriDialog,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('allowedScopes'.tr()),
|
||||||
|
Card(
|
||||||
|
margin: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
...allowedScopes.value.map(
|
||||||
|
(scope) => ListTile(
|
||||||
|
title: Text(scope),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Symbols.delete),
|
||||||
|
onPressed: () {
|
||||||
|
allowedScopes.value =
|
||||||
|
allowedScopes.value
|
||||||
|
.where(
|
||||||
|
(s) => s != scope,
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (allowedScopes.value.isNotEmpty)
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Symbols.add),
|
||||||
|
title: Text('add').tr(),
|
||||||
|
onTap: showAddScopeDialog,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text('requirePkce'.tr()),
|
||||||
|
value: requirePkce.value,
|
||||||
|
onChanged:
|
||||||
|
(value) => requirePkce.value = value,
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text('allowOfflineAccess'.tr()),
|
||||||
|
value: allowOfflineAccess.value,
|
||||||
|
onChanged:
|
||||||
|
(value) =>
|
||||||
|
allowOfflineAccess.value = value,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(horizontal: 16, bottom: 24),
|
||||||
|
isExpanded: oauthEnabled.value,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: submitting.value ? null : performAction,
|
||||||
|
label: Text('saveChanges'.tr()),
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isModal) {
|
||||||
|
return bodyContent;
|
||||||
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
title: Text(isNew ? 'createCustomApp'.tr() : 'editCustomApp'.tr()),
|
||||||
),
|
),
|
||||||
body:
|
body: bodyContent,
|
||||||
app == null && !isNew
|
|
||||||
? const Center(child: CircularProgressIndicator())
|
|
||||||
: app?.hasError == true && !isNew
|
|
||||||
? ResponseErrorWidget(
|
|
||||||
error: app!.error,
|
|
||||||
onRetry:
|
|
||||||
() => ref.invalidate(
|
|
||||||
customAppProvider(publisherName, projectId, id!),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
child: Container(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.surfaceContainerHigh,
|
|
||||||
child:
|
|
||||||
background.value != null
|
|
||||||
? CloudFileWidget(
|
|
||||||
item: background.value!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setPicture('background');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 20,
|
|
||||||
bottom: -32,
|
|
||||||
child: GestureDetector(
|
|
||||||
child: ProfilePictureWidget(
|
|
||||||
fileId: picture.value?.id,
|
|
||||||
radius: 40,
|
|
||||||
fallbackIcon: Symbols.apps,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setPicture('picture');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).padding(bottom: 32),
|
|
||||||
Form(
|
|
||||||
key: formKey,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
controller: nameController,
|
|
||||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: slugController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'slug'.tr(),
|
|
||||||
helperText: 'slugHint'.tr(),
|
|
||||||
),
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: descriptionController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'description'.tr(),
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
maxLines: 3,
|
|
||||||
onTapOutside:
|
|
||||||
(_) =>
|
|
||||||
FocusManager.instance.primaryFocus
|
|
||||||
?.unfocus(),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
ExpansionPanelList(
|
|
||||||
expansionCallback: (index, isExpanded) {
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
enableLinks.value = isExpanded;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
oauthEnabled.value = isExpanded;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
ExpansionPanel(
|
|
||||||
headerBuilder:
|
|
||||||
(context, isExpanded) =>
|
|
||||||
ListTile(title: Text('appLinks').tr()),
|
|
||||||
body: Column(
|
|
||||||
spacing: 16,
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
controller: homePageController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'homePageUrl'.tr(),
|
|
||||||
hintText: 'https://example.com',
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.url,
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: privacyPolicyController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'privacyPolicyUrl'.tr(),
|
|
||||||
hintText: 'https://example.com/privacy',
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.url,
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: termsController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'termsOfServiceUrl'.tr(),
|
|
||||||
hintText: 'https://example.com/terms',
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.url,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 16, bottom: 24),
|
|
||||||
isExpanded: enableLinks.value,
|
|
||||||
),
|
|
||||||
ExpansionPanel(
|
|
||||||
headerBuilder:
|
|
||||||
(context, isExpanded) => ListTile(
|
|
||||||
title: Text('oauthConfig').tr(),
|
|
||||||
),
|
|
||||||
body: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text('redirectUris'.tr()),
|
|
||||||
Card(
|
|
||||||
margin: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
...redirectUris.value.map(
|
|
||||||
(uri) => ListTile(
|
|
||||||
title: Text(uri),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Symbols.delete,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
redirectUris.value =
|
|
||||||
redirectUris.value
|
|
||||||
.where(
|
|
||||||
(u) => u != uri,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (redirectUris.value.isNotEmpty)
|
|
||||||
const Divider(height: 1),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Symbols.add),
|
|
||||||
title: Text('addRedirectUri'.tr()),
|
|
||||||
onTap: showAddRedirectUriDialog,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text('allowedScopes'.tr()),
|
|
||||||
Card(
|
|
||||||
margin: const EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
...allowedScopes.value.map(
|
|
||||||
(scope) => ListTile(
|
|
||||||
title: Text(scope),
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(
|
|
||||||
Symbols.delete,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
allowedScopes.value =
|
|
||||||
allowedScopes.value
|
|
||||||
.where(
|
|
||||||
(s) => s != scope,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (allowedScopes.value.isNotEmpty)
|
|
||||||
const Divider(height: 1),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Symbols.add),
|
|
||||||
title: Text('add').tr(),
|
|
||||||
onTap: showAddScopeDialog,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
SwitchListTile(
|
|
||||||
title: Text('requirePkce'.tr()),
|
|
||||||
value: requirePkce.value,
|
|
||||||
onChanged:
|
|
||||||
(value) => requirePkce.value = value,
|
|
||||||
),
|
|
||||||
SwitchListTile(
|
|
||||||
title: Text('allowOfflineAccess'.tr()),
|
|
||||||
value: allowOfflineAccess.value,
|
|
||||||
onChanged:
|
|
||||||
(value) =>
|
|
||||||
allowOfflineAccess.value = value,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 16, bottom: 24),
|
|
||||||
isExpanded: oauthEnabled.value,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton.icon(
|
|
||||||
onPressed:
|
|
||||||
submitting.value ? null : performAction,
|
|
||||||
label: Text('saveChanges'.tr()),
|
|
||||||
icon: const Icon(Symbols.save),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(all: 24),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,13 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
final String publisherName;
|
final String publisherName;
|
||||||
final String projectId;
|
final String projectId;
|
||||||
final String? id;
|
final String? id;
|
||||||
|
final bool isModal;
|
||||||
const EditBotScreen({
|
const EditBotScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.publisherName,
|
required this.publisherName,
|
||||||
required this.projectId,
|
required this.projectId,
|
||||||
this.id,
|
this.id,
|
||||||
|
this.isModal = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -191,230 +193,293 @@ class EditBotScreen extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final bodyContent =
|
||||||
|
botData == null && !isNew
|
||||||
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: botData?.hasError == true && !isNew
|
||||||
|
? ResponseErrorWidget(
|
||||||
|
error: botData!.error,
|
||||||
|
onRetry:
|
||||||
|
() => ref.invalidate(
|
||||||
|
botProvider(publisherName, projectId, id!),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 16 / 7,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
child: Container(
|
||||||
|
color:
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainerHigh,
|
||||||
|
child:
|
||||||
|
background.value != null
|
||||||
|
? CloudFileWidget(
|
||||||
|
item: background.value!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setPicture('background');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
left: 20,
|
||||||
|
bottom: -32,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: ProfilePictureWidget(
|
||||||
|
fileId: picture.value?.id,
|
||||||
|
radius: 40,
|
||||||
|
fallbackIcon: Symbols.smart_toy,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
setPicture('picture');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).padding(bottom: 32),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'name'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: nickController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'nickname'.tr(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: slugController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'slug'.tr(),
|
||||||
|
helperText: 'slugHint'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: bioController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'bio'.tr(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: firstNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'firstName'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: middleNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'middleName'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: lastNameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'lastName'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: genderController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'gender'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: pronounsController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'pronouns'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
spacing: 16,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: locationController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'location'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: timeZoneController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'timeZone'.tr(),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: birthday.value ?? DateTime.now(),
|
||||||
|
firstDate: DateTime(1900),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
);
|
||||||
|
if (date != null) {
|
||||||
|
birthday.value = date;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).dividerColor,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'birthday'.tr(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).hintColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
birthday.value != null
|
||||||
|
? DateFormat.yMMMd().format(
|
||||||
|
birthday.value!,
|
||||||
|
)
|
||||||
|
: 'Select a date'.tr(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: submitting.value ? null : performAction,
|
||||||
|
label: Text('saveChanges').tr(),
|
||||||
|
icon: const Icon(Symbols.save),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 24),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isModal) {
|
||||||
|
return bodyContent;
|
||||||
|
}
|
||||||
|
|
||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
isNoBackground: false,
|
isNoBackground: false,
|
||||||
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
appBar: AppBar(title: Text(isNew ? 'createBot'.tr() : 'editBot'.tr())),
|
||||||
body:
|
body: bodyContent,
|
||||||
botData == null && !isNew
|
|
||||||
? const Center(child: CircularProgressIndicator())
|
|
||||||
: botData?.hasError == true && !isNew
|
|
||||||
? ResponseErrorWidget(
|
|
||||||
error: botData!.error,
|
|
||||||
onRetry:
|
|
||||||
() => ref.invalidate(
|
|
||||||
botProvider(publisherName, projectId, id!),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
AspectRatio(
|
|
||||||
aspectRatio: 16 / 7,
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
fit: StackFit.expand,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
child: Container(
|
|
||||||
color:
|
|
||||||
Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.surfaceContainerHigh,
|
|
||||||
child:
|
|
||||||
background.value != null
|
|
||||||
? CloudFileWidget(
|
|
||||||
item: background.value!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setPicture('background');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
left: 20,
|
|
||||||
bottom: -32,
|
|
||||||
child: GestureDetector(
|
|
||||||
child: ProfilePictureWidget(
|
|
||||||
fileId: picture.value?.id,
|
|
||||||
radius: 40,
|
|
||||||
fallbackIcon: Symbols.smart_toy,
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
setPicture('picture');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
).padding(bottom: 32),
|
|
||||||
Form(
|
|
||||||
key: formKey,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
TextFormField(
|
|
||||||
controller: nameController,
|
|
||||||
decoration: InputDecoration(labelText: 'name'.tr()),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: nickController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'nickname'.tr(),
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: slugController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'slug'.tr(),
|
|
||||||
helperText: 'slugHint'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextFormField(
|
|
||||||
controller: bioController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'bio'.tr(),
|
|
||||||
alignLabelWithHint: true,
|
|
||||||
),
|
|
||||||
maxLines: 3,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
spacing: 16,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: firstNameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'firstName'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: middleNameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'middleName'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: lastNameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'lastName'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
spacing: 16,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: genderController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'gender'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: pronounsController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'pronouns'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
spacing: 16,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: locationController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'location'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: TextFormField(
|
|
||||||
controller: timeZoneController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'timeZone'.tr(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () async {
|
|
||||||
final date = await showDatePicker(
|
|
||||||
context: context,
|
|
||||||
initialDate: birthday.value ?? DateTime.now(),
|
|
||||||
firstDate: DateTime(1900),
|
|
||||||
lastDate: DateTime.now(),
|
|
||||||
);
|
|
||||||
if (date != null) {
|
|
||||||
birthday.value = date;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color: Theme.of(context).dividerColor,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'birthday'.tr(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).hintColor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
birthday.value != null
|
|
||||||
? DateFormat.yMMMd().format(
|
|
||||||
birthday.value!,
|
|
||||||
)
|
|
||||||
: 'Select a date'.tr(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: TextButton.icon(
|
|
||||||
onPressed:
|
|
||||||
submitting.value ? null : performAction,
|
|
||||||
label: Text('saveChanges').tr(),
|
|
||||||
icon: const Icon(Symbols.save),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
).padding(all: 24),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:dropdown_button2/dropdown_button2.dart';
|
import 'package:dropdown_button2/dropdown_button2.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -99,15 +97,6 @@ class DeveloperHubScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
if (currentProject.value == null)
|
|
||||||
...([
|
|
||||||
// Welcome Section
|
|
||||||
_WelcomeSection(currentDeveloper: currentDeveloper.value),
|
|
||||||
|
|
||||||
// Navigation Tabs
|
|
||||||
_NavigationTabs(),
|
|
||||||
]),
|
|
||||||
|
|
||||||
// Main Content
|
// Main Content
|
||||||
if (currentProject.value != null)
|
if (currentProject.value != null)
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -195,162 +184,6 @@ class _ConsoleAppBar extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Welcome Section
|
|
||||||
class _WelcomeSection extends StatelessWidget {
|
|
||||||
final SnDeveloper? currentDeveloper;
|
|
||||||
|
|
||||||
const _WelcomeSection({required this.currentDeveloper});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors:
|
|
||||||
isDark
|
|
||||||
? [
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
|
||||||
Theme.of(context).colorScheme.surfaceContainerLow,
|
|
||||||
]
|
|
||||||
: [const Color(0xFFE8F0FE), const Color(0xFFF1F3F4)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
right: 16,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
child: _RandomStickerImage(
|
|
||||||
width: 180,
|
|
||||||
height: 180,
|
|
||||||
).opacity(isWideScreen(context) ? 1 : 0.5),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: 180,
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Good morning!',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: FontWeight.w400,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Gap(4),
|
|
||||||
Text(
|
|
||||||
currentDeveloper != null
|
|
||||||
? "You're working as ${currentDeveloper!.publisher!.nick}"
|
|
||||||
: "Choose a developer and continue.",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Random Sticker Image Widget
|
|
||||||
class _RandomStickerImage extends StatelessWidget {
|
|
||||||
final double? width;
|
|
||||||
final double? height;
|
|
||||||
|
|
||||||
const _RandomStickerImage({this.width, this.height});
|
|
||||||
|
|
||||||
static const List<String> _stickers = [
|
|
||||||
'assets/images/stickers/clap.png',
|
|
||||||
'assets/images/stickers/confuse.png',
|
|
||||||
'assets/images/stickers/pray.png',
|
|
||||||
'assets/images/stickers/thumb_up.png',
|
|
||||||
];
|
|
||||||
|
|
||||||
String _getRandomSticker() {
|
|
||||||
final random = Random();
|
|
||||||
return _stickers[random.nextInt(_stickers.length)];
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Image.asset(
|
|
||||||
_getRandomSticker(),
|
|
||||||
width: width ?? 80,
|
|
||||||
height: height ?? 80,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation Tabs
|
|
||||||
class _NavigationTabs extends StatelessWidget {
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(color: Theme.of(context).colorScheme.surface),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Gap(24),
|
|
||||||
_NavTabItem(title: 'Dashboard', isActive: true),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NavTabItem extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final bool isActive;
|
|
||||||
|
|
||||||
const _NavTabItem({required this.title, this.isActive = false});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: BorderSide(
|
|
||||||
color:
|
|
||||||
isActive
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: Colors.transparent,
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
color:
|
|
||||||
isActive
|
|
||||||
? Theme.of(context).colorScheme.primary
|
|
||||||
: Theme.of(context).colorScheme.onSurface,
|
|
||||||
fontWeight: isActive ? FontWeight.w500 : FontWeight.w400,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main Content Section
|
// Main Content Section
|
||||||
class _MainContentSection extends HookConsumerWidget {
|
class _MainContentSection extends HookConsumerWidget {
|
||||||
final SnDeveloper? currentDeveloper;
|
final SnDeveloper? currentDeveloper;
|
||||||
|
|||||||
@@ -4,10 +4,20 @@ import 'package:island/screens/developers/edit_app.dart';
|
|||||||
class NewCustomAppScreen extends StatelessWidget {
|
class NewCustomAppScreen extends StatelessWidget {
|
||||||
final String publisherName;
|
final String publisherName;
|
||||||
final String projectId;
|
final String projectId;
|
||||||
const NewCustomAppScreen({super.key, required this.publisherName, required this.projectId});
|
final bool isModal;
|
||||||
|
const NewCustomAppScreen({
|
||||||
|
super.key,
|
||||||
|
required this.publisherName,
|
||||||
|
required this.projectId,
|
||||||
|
this.isModal = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return EditAppScreen(publisherName: publisherName, projectId: projectId);
|
return EditAppScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
isModal: isModal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:island/screens/developers/edit_bot.dart';
|
import 'package:island/screens/developers/edit_bot.dart';
|
||||||
|
|
||||||
class NewBotScreen extends StatelessWidget {
|
class NewBotScreen extends StatelessWidget {
|
||||||
final String publisherName;
|
final String publisherName;
|
||||||
final String projectId;
|
final String projectId;
|
||||||
const NewBotScreen({super.key, required this.publisherName, required this.projectId});
|
final bool isModal;
|
||||||
|
const NewBotScreen({
|
||||||
|
super.key,
|
||||||
|
required this.publisherName,
|
||||||
|
required this.projectId,
|
||||||
|
this.isModal = false,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return EditBotScreen(publisherName: publisherName, projectId: projectId);
|
return EditBotScreen(
|
||||||
|
publisherName: publisherName,
|
||||||
|
projectId: projectId,
|
||||||
|
isModal: isModal,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/dev_project.dart';
|
import 'package:island/models/dev_project.dart';
|
||||||
import 'package:island/screens/developers/apps.dart';
|
import 'package:island/screens/developers/apps.dart';
|
||||||
import 'package:island/screens/developers/bots.dart';
|
import 'package:island/screens/developers/bots.dart';
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
|
||||||
class ProjectDetailView extends HookConsumerWidget {
|
class ProjectDetailView extends HookConsumerWidget {
|
||||||
final String publisherName;
|
final String publisherName;
|
||||||
@@ -54,6 +56,37 @@ class ProjectDetailView extends HookConsumerWidget {
|
|||||||
label: Text('bots'.tr()),
|
label: Text('bots'.tr()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
leading: Container(
|
||||||
|
width: isWiderScreen(context) ? 256 : 80,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 24,
|
||||||
|
right: 24,
|
||||||
|
bottom: 8,
|
||||||
|
top: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: BorderSide(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurface.withOpacity(0.12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
spacing: 8,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: onBackToHub,
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
iconSize: 16,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
if (isWiderScreen(context))
|
||||||
|
Expanded(child: Text("backToHub").tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -69,6 +102,7 @@ class ProjectDetailView extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const Gap(4),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -97,7 +131,7 @@ class ProjectDetailView extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
BotsScreen(publisherName: publisherName, projectId: project.id),
|
BotsScreen(publisherName: publisherName, projectId: project.id),
|
||||||
],
|
],
|
||||||
),
|
).padding(horizontal: 8),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -737,7 +737,7 @@ class ActivityListNotifier extends _$ActivityListNotifier
|
|||||||
};
|
};
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
'/sphere/activities',
|
'/sphere/timeline',
|
||||||
queryParameters: queryParameters,
|
queryParameters: queryParameters,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'explore.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$activityListNotifierHash() =>
|
String _$activityListNotifierHash() =>
|
||||||
r'a3ad3242f08139bef14a2f0fab6591ce8b3cb9f0';
|
r'77ffc7852feffa5438b56fa26123d453b7c310cf';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.add_circle,
|
Symbols.remove_circle,
|
||||||
),
|
),
|
||||||
label: Text('unsubscribe'.tr()),
|
label: Text('unsubscribe'.tr()),
|
||||||
)
|
)
|
||||||
@@ -214,7 +214,7 @@ class PostCategoryDetailScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(
|
icon: const Icon(
|
||||||
Symbols.remove_circle,
|
Symbols.add_circle,
|
||||||
),
|
),
|
||||||
label: Text('subscribe'.tr()),
|
label: Text('subscribe'.tr()),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -326,21 +326,263 @@ class _PublisherHeatmapWidget extends StatelessWidget {
|
|||||||
|
|
||||||
class _PublisherCategoryTabWidget extends StatelessWidget {
|
class _PublisherCategoryTabWidget extends StatelessWidget {
|
||||||
final TabController categoryTabController;
|
final TabController categoryTabController;
|
||||||
|
final ValueNotifier<bool?> includeReplies;
|
||||||
|
final ValueNotifier<bool> mediaOnly;
|
||||||
|
final ValueNotifier<String?> queryTerm;
|
||||||
|
final ValueNotifier<String?> order;
|
||||||
|
final ValueNotifier<bool> orderDesc;
|
||||||
|
final ValueNotifier<int?> periodStart;
|
||||||
|
final ValueNotifier<int?> periodEnd;
|
||||||
|
final ValueNotifier<bool> showAdvancedFilters;
|
||||||
|
|
||||||
const _PublisherCategoryTabWidget({required this.categoryTabController});
|
const _PublisherCategoryTabWidget({
|
||||||
|
required this.categoryTabController,
|
||||||
|
required this.includeReplies,
|
||||||
|
required this.mediaOnly,
|
||||||
|
required this.queryTerm,
|
||||||
|
required this.order,
|
||||||
|
required this.orderDesc,
|
||||||
|
required this.periodStart,
|
||||||
|
required this.periodEnd,
|
||||||
|
required this.showAdvancedFilters,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
margin: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
child: TabBar(
|
child: Column(
|
||||||
controller: categoryTabController,
|
children: [
|
||||||
dividerColor: Colors.transparent,
|
TabBar(
|
||||||
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
controller: categoryTabController,
|
||||||
tabs: [
|
dividerColor: Colors.transparent,
|
||||||
Tab(text: 'all'.tr()),
|
splashBorderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||||
Tab(text: 'postTypePost'.tr()),
|
tabs: [
|
||||||
Tab(text: 'postArticle'.tr()),
|
Tab(text: 'all'.tr()),
|
||||||
|
Tab(text: 'postTypePost'.tr()),
|
||||||
|
Tab(text: 'postArticle'.tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: CheckboxListTile(
|
||||||
|
title: Text('reply'.tr()),
|
||||||
|
value: includeReplies.value,
|
||||||
|
tristate: true,
|
||||||
|
onChanged: (value) {
|
||||||
|
// Cycle through: null -> false -> true -> null
|
||||||
|
if (includeReplies.value == null) {
|
||||||
|
includeReplies.value = false;
|
||||||
|
} else if (includeReplies.value == false) {
|
||||||
|
includeReplies.value = true;
|
||||||
|
} else {
|
||||||
|
includeReplies.value = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
secondary: const Icon(Symbols.reply),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: CheckboxListTile(
|
||||||
|
title: Text('attachments'.tr()),
|
||||||
|
value: mediaOnly.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
mediaOnly.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
secondary: const Icon(Symbols.attachment),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
CheckboxListTile(
|
||||||
|
title: Text('descendingOrder'.tr()),
|
||||||
|
value: orderDesc.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value != null) {
|
||||||
|
orderDesc.value = value;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dense: true,
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
secondary: const Icon(Symbols.sort),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
ListTile(
|
||||||
|
title: Text('advancedFilters'.tr()),
|
||||||
|
leading: const Icon(Symbols.filter_list),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(const Radius.circular(8)),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
showAdvancedFilters.value
|
||||||
|
? Symbols.expand_less
|
||||||
|
: Symbols.expand_more,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
showAdvancedFilters.value = !showAdvancedFilters.value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (showAdvancedFilters.value) ...[
|
||||||
|
const Divider(height: 1),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'search'.tr(),
|
||||||
|
hintText: 'searchPosts'.tr(),
|
||||||
|
prefixIcon: const Icon(Symbols.search),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
queryTerm.value = value.isEmpty ? null : value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'sortBy'.tr(),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
value: order.value,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(value: 'date', child: Text('date'.tr())),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'popularity',
|
||||||
|
child: Text('popularity'.tr()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
order.value = value;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(12),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
final pickedDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate:
|
||||||
|
periodStart.value != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
periodStart.value! * 1000,
|
||||||
|
)
|
||||||
|
: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime.now().add(
|
||||||
|
const Duration(days: 365),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (pickedDate != null) {
|
||||||
|
periodStart.value =
|
||||||
|
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'fromDate'.tr(),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
suffixIcon: const Icon(Symbols.calendar_today),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
periodStart.value != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
periodStart.value! * 1000,
|
||||||
|
).toString().split(' ')[0]
|
||||||
|
: 'selectDate'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
Expanded(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () async {
|
||||||
|
final pickedDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate:
|
||||||
|
periodEnd.value != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
periodEnd.value! * 1000,
|
||||||
|
)
|
||||||
|
: DateTime.now(),
|
||||||
|
firstDate: DateTime(2000),
|
||||||
|
lastDate: DateTime.now().add(
|
||||||
|
const Duration(days: 365),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (pickedDate != null) {
|
||||||
|
periodEnd.value =
|
||||||
|
pickedDate.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: InputDecorator(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'toDate'.tr(),
|
||||||
|
border: const OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
suffixIcon: const Icon(Symbols.calendar_today),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
periodEnd.value != null
|
||||||
|
? DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
periodEnd.value! * 1000,
|
||||||
|
).toString().split(' ')[0]
|
||||||
|
: 'selectDate'.tr(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -423,7 +665,18 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
categoryTab.value = categoryTabController.index;
|
categoryTab.value = categoryTabController.index;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final includeReplies = useState<bool?>(null);
|
||||||
|
final mediaOnly = useState(false);
|
||||||
|
final queryTerm = useState<String?>(null);
|
||||||
|
final order = useState<String?>('date'); // 'popularity' or 'date'
|
||||||
|
final orderDesc = useState(
|
||||||
|
true,
|
||||||
|
); // true for descending, false for ascending
|
||||||
|
final periodStart = useState<int?>(null);
|
||||||
|
final periodEnd = useState<int?>(null);
|
||||||
|
final showAdvancedFilters = useState(false);
|
||||||
final subscribing = useState(false);
|
final subscribing = useState(false);
|
||||||
|
final isPinnedExpanded = useState(true);
|
||||||
|
|
||||||
Future<void> subscribe() async {
|
Future<void> subscribe() async {
|
||||||
final apiClient = ref.watch(apiClientProvider);
|
final apiClient = ref.watch(apiClientProvider);
|
||||||
@@ -494,21 +747,66 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverGap(16),
|
SliverGap(16),
|
||||||
SliverPostList(pubName: name, pinned: true),
|
SliverToBoxAdapter(
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text('pinnedPosts'.tr()),
|
||||||
|
leading: const Icon(Symbols.push_pin),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: Icon(
|
||||||
|
isPinnedExpanded.value
|
||||||
|
? Symbols.expand_less
|
||||||
|
: Symbols.expand_more,
|
||||||
|
),
|
||||||
|
onTap:
|
||||||
|
() =>
|
||||||
|
isPinnedExpanded.value =
|
||||||
|
!isPinnedExpanded.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...[
|
||||||
|
if (isPinnedExpanded.value)
|
||||||
|
SliverPostList(pubName: name, pinned: true),
|
||||||
|
],
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _PublisherCategoryTabWidget(
|
child: _PublisherCategoryTabWidget(
|
||||||
categoryTabController: categoryTabController,
|
categoryTabController: categoryTabController,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
orderDesc: orderDesc,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
showAdvancedFilters: showAdvancedFilters,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPostList(
|
SliverPostList(
|
||||||
key: ValueKey(categoryTab.value),
|
key: ValueKey(
|
||||||
|
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
||||||
|
),
|
||||||
pubName: name,
|
pubName: name,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
type: switch (categoryTab.value) {
|
type:
|
||||||
1 => 0,
|
categoryTab.value == 1
|
||||||
2 => 1,
|
? 0
|
||||||
_ => null,
|
: (categoryTab.value == 2 ? 1 : null),
|
||||||
},
|
includeReplies: includeReplies.value,
|
||||||
|
mediaOnly: mediaOnly.value,
|
||||||
|
queryTerm: queryTerm.value,
|
||||||
|
order: order.value,
|
||||||
|
orderDesc: orderDesc.value,
|
||||||
|
periodStart: periodStart.value,
|
||||||
|
periodEnd: periodEnd.value,
|
||||||
),
|
),
|
||||||
SliverGap(
|
SliverGap(
|
||||||
MediaQuery.of(context).padding.bottom + 16,
|
MediaQuery.of(context).padding.bottom + 16,
|
||||||
@@ -617,21 +915,60 @@ class PublisherProfileScreen extends HookConsumerWidget {
|
|||||||
heatmap: heatmap,
|
heatmap: heatmap,
|
||||||
).padding(vertical: 4),
|
).padding(vertical: 4),
|
||||||
),
|
),
|
||||||
SliverPostList(pubName: name, pinned: true),
|
SliverToBoxAdapter(
|
||||||
|
child: Card(
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
child: ListTile(
|
||||||
|
title: Text('pinnedPosts'.tr()),
|
||||||
|
trailing: Icon(
|
||||||
|
isPinnedExpanded.value
|
||||||
|
? Symbols.expand_less
|
||||||
|
: Symbols.expand_more,
|
||||||
|
),
|
||||||
|
onTap:
|
||||||
|
() =>
|
||||||
|
isPinnedExpanded.value =
|
||||||
|
!isPinnedExpanded.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
...[
|
||||||
|
if (isPinnedExpanded.value)
|
||||||
|
SliverPostList(pubName: name, pinned: true),
|
||||||
|
],
|
||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: _PublisherCategoryTabWidget(
|
child: _PublisherCategoryTabWidget(
|
||||||
categoryTabController: categoryTabController,
|
categoryTabController: categoryTabController,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
orderDesc: orderDesc,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
showAdvancedFilters: showAdvancedFilters,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SliverPostList(
|
SliverPostList(
|
||||||
key: ValueKey(categoryTab.value),
|
key: ValueKey(
|
||||||
|
'${categoryTab.value}-${includeReplies.value}-${mediaOnly.value}-${queryTerm.value}-${order.value}-${orderDesc.value}-${periodStart.value}-${periodEnd.value}',
|
||||||
|
),
|
||||||
pubName: name,
|
pubName: name,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
type: switch (categoryTab.value) {
|
type:
|
||||||
1 => 0,
|
categoryTab.value == 1
|
||||||
2 => 1,
|
? 0
|
||||||
_ => null,
|
: (categoryTab.value == 2 ? 1 : null),
|
||||||
},
|
includeReplies: includeReplies.value,
|
||||||
|
mediaOnly: mediaOnly.value,
|
||||||
|
queryTerm: queryTerm.value,
|
||||||
|
order: order.value,
|
||||||
|
orderDesc: orderDesc.value,
|
||||||
|
periodStart: periodStart.value,
|
||||||
|
periodEnd: periodEnd.value,
|
||||||
),
|
),
|
||||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
|
import "dart:math" as math;
|
||||||
import "package:dio/dio.dart";
|
import "package:dio/dio.dart";
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
@@ -57,6 +58,9 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
final listController = useMemoized(() => ListController(), []);
|
||||||
|
|
||||||
|
// Scroll animation notifiers
|
||||||
|
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||||
|
|
||||||
// Update local thoughts when provider data changes
|
// Update local thoughts when provider data changes
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
thoughts.whenData((data) {
|
thoughts.whenData((data) {
|
||||||
@@ -86,6 +90,20 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [localThoughts.value.length, isStreaming.value]);
|
}, [localThoughts.value.length, isStreaming.value]);
|
||||||
|
|
||||||
|
// Add scroll listener for gradient animations
|
||||||
|
useEffect(() {
|
||||||
|
void onScroll() {
|
||||||
|
// Update gradient animations
|
||||||
|
final pixels = scrollController.position.pixels;
|
||||||
|
|
||||||
|
// Bottom gradient: appears when not at bottom (pixels > 0)
|
||||||
|
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(onScroll);
|
||||||
|
return () => scrollController.removeListener(onScroll);
|
||||||
|
}, [scrollController]);
|
||||||
|
|
||||||
void sendMessage() async {
|
void sendMessage() async {
|
||||||
if (messageController.text.trim().isEmpty) return;
|
if (messageController.text.trim().isEmpty) return;
|
||||||
|
|
||||||
@@ -258,65 +276,120 @@ class ThoughtScreen extends HookConsumerWidget {
|
|||||||
const Gap(8),
|
const Gap(8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Stack(
|
||||||
child: Container(
|
children: [
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
// Thoughts list
|
||||||
child: Column(
|
Center(
|
||||||
children: [
|
child: Container(
|
||||||
Expanded(
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
child: thoughts.when(
|
child: Column(
|
||||||
data:
|
children: [
|
||||||
(thoughtList) => SuperListView.builder(
|
Expanded(
|
||||||
listController: listController,
|
child: thoughts.when(
|
||||||
controller: scrollController,
|
data:
|
||||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
(thoughtList) => SuperListView.builder(
|
||||||
reverse: true,
|
listController: listController,
|
||||||
itemCount:
|
controller: scrollController,
|
||||||
localThoughts.value.length +
|
padding: EdgeInsets.only(
|
||||||
(isStreaming.value ? 1 : 0),
|
top: 16,
|
||||||
itemBuilder: (context, index) {
|
bottom:
|
||||||
if (isStreaming.value && index == 0) {
|
MediaQuery.of(context).padding.bottom +
|
||||||
return ThoughtItem(
|
80, // Leave space for thought input
|
||||||
isStreaming: true,
|
),
|
||||||
streamingText: streamingText.value,
|
reverse: true,
|
||||||
reasoningChunks: reasoningChunks.value,
|
itemCount:
|
||||||
streamingFunctionCalls: functionCalls.value,
|
localThoughts.value.length +
|
||||||
);
|
(isStreaming.value ? 1 : 0),
|
||||||
}
|
itemBuilder: (context, index) {
|
||||||
final thoughtIndex =
|
if (isStreaming.value && index == 0) {
|
||||||
isStreaming.value ? index - 1 : index;
|
return ThoughtItem(
|
||||||
final thought = localThoughts.value[thoughtIndex];
|
isStreaming: true,
|
||||||
return ThoughtItem(
|
streamingText: streamingText.value,
|
||||||
thought: thought,
|
reasoningChunks: reasoningChunks.value,
|
||||||
thoughtIndex: thoughtIndex,
|
streamingFunctionCalls: functionCalls.value,
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
|
final thoughtIndex =
|
||||||
|
isStreaming.value ? index - 1 : index;
|
||||||
|
final thought = localThoughts.value[thoughtIndex];
|
||||||
|
return ThoughtItem(
|
||||||
|
thought: thought,
|
||||||
|
thoughtIndex: thoughtIndex,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
loading:
|
||||||
|
() =>
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
error:
|
||||||
|
(error, _) => ResponseErrorWidget(
|
||||||
|
error: error,
|
||||||
|
onRetry:
|
||||||
|
() =>
|
||||||
|
selectedSequenceId.value != null
|
||||||
|
? ref.invalidate(
|
||||||
|
thoughtSequenceProvider(
|
||||||
|
selectedSequenceId.value!,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: bottomGradientNotifier.value,
|
||||||
|
builder:
|
||||||
|
(context, child) => Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: bottomGradientNotifier.value.value,
|
||||||
|
child: Container(
|
||||||
|
height: math.min(
|
||||||
|
MediaQuery.of(context).size.height * 0.1,
|
||||||
|
128,
|
||||||
),
|
),
|
||||||
loading:
|
decoration: BoxDecoration(
|
||||||
() => const Center(child: CircularProgressIndicator()),
|
gradient: LinearGradient(
|
||||||
error:
|
begin: Alignment.bottomCenter,
|
||||||
(error, _) => ResponseErrorWidget(
|
end: Alignment.topCenter,
|
||||||
error: error,
|
colors: [
|
||||||
onRetry:
|
Theme.of(
|
||||||
() =>
|
context,
|
||||||
selectedSequenceId.value != null
|
).colorScheme.surfaceContainer.withOpacity(0.8),
|
||||||
? ref.invalidate(
|
Theme.of(
|
||||||
thoughtSequenceProvider(
|
context,
|
||||||
selectedSequenceId.value!,
|
).colorScheme.surfaceContainer.withOpacity(0.0),
|
||||||
),
|
],
|
||||||
)
|
),
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Thought Input positioned above gradient (higher z-index)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0, // At the very bottom, above gradient
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: ThoughtInput(
|
||||||
|
messageController: messageController,
|
||||||
|
isStreaming: isStreaming.value,
|
||||||
|
onSend: sendMessage,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ThoughtInput(
|
),
|
||||||
messageController: messageController,
|
|
||||||
isStreaming: isStreaming.value,
|
|
||||||
onSend: sendMessage,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import "dart:convert";
|
import "dart:convert";
|
||||||
|
import "dart:math" as math;
|
||||||
import "package:dio/dio.dart";
|
import "package:dio/dio.dart";
|
||||||
import "package:easy_localization/easy_localization.dart";
|
import "package:easy_localization/easy_localization.dart";
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
@@ -54,6 +55,9 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
final listController = useMemoized(() => ListController(), []);
|
final listController = useMemoized(() => ListController(), []);
|
||||||
|
|
||||||
|
// Scroll animation notifiers
|
||||||
|
final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
|
||||||
|
|
||||||
// Scroll to bottom when thoughts change or streaming state changes
|
// Scroll to bottom when thoughts change or streaming state changes
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
if (localThoughts.value.isNotEmpty || isStreaming.value) {
|
if (localThoughts.value.isNotEmpty || isStreaming.value) {
|
||||||
@@ -68,6 +72,20 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
return null;
|
return null;
|
||||||
}, [localThoughts.value.length, isStreaming.value]);
|
}, [localThoughts.value.length, isStreaming.value]);
|
||||||
|
|
||||||
|
// Add scroll listener for gradient animations
|
||||||
|
useEffect(() {
|
||||||
|
void onScroll() {
|
||||||
|
// Update gradient animations
|
||||||
|
final pixels = scrollController.position.pixels;
|
||||||
|
|
||||||
|
// Bottom gradient: appears when not at bottom (pixels > 0)
|
||||||
|
bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(onScroll);
|
||||||
|
return () => scrollController.removeListener(onScroll);
|
||||||
|
}, [scrollController]);
|
||||||
|
|
||||||
void sendMessage() async {
|
void sendMessage() async {
|
||||||
if (messageController.text.trim().isEmpty) return;
|
if (messageController.text.trim().isEmpty) return;
|
||||||
|
|
||||||
@@ -196,47 +214,103 @@ class ThoughtSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
return SheetScaffold(
|
return SheetScaffold(
|
||||||
titleText: currentTopic.value ?? 'aiThought'.tr(),
|
titleText: currentTopic.value ?? 'aiThought'.tr(),
|
||||||
child: Center(
|
child: Stack(
|
||||||
child: Container(
|
children: [
|
||||||
constraints: BoxConstraints(maxWidth: 640),
|
// Thoughts list
|
||||||
child: Column(
|
Center(
|
||||||
children: [
|
child: Container(
|
||||||
Expanded(
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
child: SuperListView.builder(
|
child: Column(
|
||||||
listController: listController,
|
children: [
|
||||||
controller: scrollController,
|
Expanded(
|
||||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
child: SuperListView.builder(
|
||||||
reverse: true,
|
listController: listController,
|
||||||
itemCount:
|
controller: scrollController,
|
||||||
localThoughts.value.length + (isStreaming.value ? 1 : 0),
|
padding: EdgeInsets.only(
|
||||||
itemBuilder: (context, index) {
|
top: 16,
|
||||||
if (isStreaming.value && index == 0) {
|
bottom:
|
||||||
return ThoughtItem(
|
MediaQuery.of(context).padding.bottom +
|
||||||
isStreaming: true,
|
80, // Leave space for thought input
|
||||||
streamingText: streamingText.value,
|
),
|
||||||
reasoningChunks: reasoningChunks.value,
|
reverse: true,
|
||||||
streamingFunctionCalls: functionCalls.value,
|
itemCount:
|
||||||
);
|
localThoughts.value.length +
|
||||||
}
|
(isStreaming.value ? 1 : 0),
|
||||||
final thoughtIndex = isStreaming.value ? index - 1 : index;
|
itemBuilder: (context, index) {
|
||||||
final thought = localThoughts.value[thoughtIndex];
|
if (isStreaming.value && index == 0) {
|
||||||
return ThoughtItem(
|
return ThoughtItem(
|
||||||
thought: thought,
|
isStreaming: true,
|
||||||
thoughtIndex: thoughtIndex,
|
streamingText: streamingText.value,
|
||||||
);
|
reasoningChunks: reasoningChunks.value,
|
||||||
},
|
streamingFunctionCalls: functionCalls.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final thoughtIndex =
|
||||||
|
isStreaming.value ? index - 1 : index;
|
||||||
|
final thought = localThoughts.value[thoughtIndex];
|
||||||
|
return ThoughtItem(
|
||||||
|
thought: thought,
|
||||||
|
thoughtIndex: thoughtIndex,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
|
||||||
|
AnimatedBuilder(
|
||||||
|
animation: bottomGradientNotifier.value,
|
||||||
|
builder:
|
||||||
|
(context, child) => Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: bottomGradientNotifier.value.value,
|
||||||
|
child: Container(
|
||||||
|
height: math.min(
|
||||||
|
MediaQuery.of(context).size.height * 0.1,
|
||||||
|
128,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainer.withOpacity(0.8),
|
||||||
|
Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.surfaceContainer.withOpacity(0.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Thought Input positioned above gradient (higher z-index)
|
||||||
|
Positioned(
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0, // At the very bottom, above gradient
|
||||||
|
child: Center(
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: 640),
|
||||||
|
child: ThoughtInput(
|
||||||
|
messageController: messageController,
|
||||||
|
isStreaming: isStreaming.value,
|
||||||
|
onSend: sendMessage,
|
||||||
|
attachedMessages: attachedMessages,
|
||||||
|
attachedPosts: attachedPosts,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ThoughtInput(
|
),
|
||||||
messageController: messageController,
|
|
||||||
isStreaming: isStreaming.value,
|
|
||||||
onSend: sendMessage,
|
|
||||||
attachedMessages: attachedMessages,
|
|
||||||
attachedPosts: attachedPosts,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ class TrayService {
|
|||||||
await trayManager.setIcon(
|
await trayManager.setIcon(
|
||||||
Platform.isWindows
|
Platform.isWindows
|
||||||
? 'assets/icons/icon.ico'
|
? 'assets/icons/icon.ico'
|
||||||
: 'assets/icons/icon-outline.svg',
|
: 'assets/icons/icon-tray.png',
|
||||||
|
isTemplate: Platform.isMacOS,
|
||||||
);
|
);
|
||||||
|
|
||||||
final menu = Menu(
|
final menu = Menu(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
|
||||||
import 'package:cross_file/cross_file.dart';
|
import 'package:cross_file/cross_file.dart';
|
||||||
import 'package:crypto/crypto.dart';
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
@@ -16,16 +15,15 @@ class FileUploader {
|
|||||||
|
|
||||||
FileUploader(this._client);
|
FileUploader(this._client);
|
||||||
|
|
||||||
/// Calculates the MD5 hash of a file.
|
/// Calculates the MD5 hash of file bytes.
|
||||||
Future<String> _calculateFileHash(XFile file) async {
|
String _calculateFileHash(Uint8List bytes) {
|
||||||
final bytes = await file.readAsBytes();
|
|
||||||
final digest = md5.convert(bytes);
|
final digest = md5.convert(bytes);
|
||||||
return digest.toString();
|
return digest.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an upload task for the given file.
|
/// Creates an upload task for the given file.
|
||||||
Future<Map<String, dynamic>> createUploadTask({
|
Future<Map<String, dynamic>> createUploadTask({
|
||||||
required XFile file,
|
required Uint8List bytes,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String contentType,
|
required String contentType,
|
||||||
String? poolId,
|
String? poolId,
|
||||||
@@ -34,8 +32,8 @@ class FileUploader {
|
|||||||
String? expiredAt,
|
String? expiredAt,
|
||||||
int? chunkSize,
|
int? chunkSize,
|
||||||
}) async {
|
}) async {
|
||||||
final hash = await _calculateFileHash(file);
|
final hash = _calculateFileHash(bytes);
|
||||||
final fileSize = await file.length();
|
final fileSize = bytes.length;
|
||||||
|
|
||||||
final response = await _client.post(
|
final response = await _client.post(
|
||||||
'/drive/files/upload/create',
|
'/drive/files/upload/create',
|
||||||
@@ -83,7 +81,7 @@ class FileUploader {
|
|||||||
|
|
||||||
/// Uploads a file in chunks using the multi-part API.
|
/// Uploads a file in chunks using the multi-part API.
|
||||||
Future<SnCloudFile> uploadFile({
|
Future<SnCloudFile> uploadFile({
|
||||||
required XFile file,
|
required Uint8List bytes,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
required String contentType,
|
required String contentType,
|
||||||
String? poolId,
|
String? poolId,
|
||||||
@@ -94,7 +92,7 @@ class FileUploader {
|
|||||||
}) async {
|
}) async {
|
||||||
// Step 1: Create upload task
|
// Step 1: Create upload task
|
||||||
final createResponse = await createUploadTask(
|
final createResponse = await createUploadTask(
|
||||||
file: file,
|
bytes: bytes,
|
||||||
fileName: fileName,
|
fileName: fileName,
|
||||||
contentType: contentType,
|
contentType: contentType,
|
||||||
poolId: poolId,
|
poolId: poolId,
|
||||||
@@ -114,24 +112,10 @@ class FileUploader {
|
|||||||
final chunksCount = createResponse['chunks_count'] as int;
|
final chunksCount = createResponse['chunks_count'] as int;
|
||||||
|
|
||||||
// Step 2: Upload chunks
|
// Step 2: Upload chunks
|
||||||
final stream = file.openRead();
|
|
||||||
final chunks = <Uint8List>[];
|
final chunks = <Uint8List>[];
|
||||||
int bytesRead = 0;
|
for (int i = 0; i < bytes.length; i += chunkSize) {
|
||||||
final buffer = BytesBuilder();
|
final end = i + chunkSize > bytes.length ? bytes.length : i + chunkSize;
|
||||||
|
chunks.add(Uint8List.fromList(bytes.sublist(i, end)));
|
||||||
await for (final chunk in stream) {
|
|
||||||
buffer.add(chunk);
|
|
||||||
bytesRead += chunk.length;
|
|
||||||
|
|
||||||
if (bytesRead >= chunkSize) {
|
|
||||||
chunks.add(buffer.takeBytes());
|
|
||||||
bytesRead = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add remaining bytes as last chunk
|
|
||||||
if (buffer.length > 0) {
|
|
||||||
chunks.add(buffer.takeBytes());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have the correct number of chunks
|
// Ensure we have the correct number of chunks
|
||||||
@@ -225,20 +209,34 @@ class FileUploader {
|
|||||||
Completer<SnCloudFile?> completer,
|
Completer<SnCloudFile?> completer,
|
||||||
) {
|
) {
|
||||||
String actualMimetype = getMimeType(fileData);
|
String actualMimetype = getMimeType(fileData);
|
||||||
late XFile file;
|
|
||||||
String actualFilename = fileData.displayName ?? 'randomly_file';
|
String actualFilename = fileData.displayName ?? 'randomly_file';
|
||||||
Uint8List? byteData;
|
Uint8List? bytes;
|
||||||
|
|
||||||
// Handle the data based on what's in the UniversalFile
|
// Handle the data based on what's in the UniversalFile
|
||||||
final data = fileData.data;
|
final data = fileData.data;
|
||||||
|
|
||||||
if (data is XFile) {
|
if (data is XFile) {
|
||||||
file = data;
|
// Read bytes from XFile
|
||||||
actualFilename = fileData.displayName ?? data.name;
|
data
|
||||||
|
.readAsBytes()
|
||||||
|
.then((readBytes) {
|
||||||
|
_performUpload(
|
||||||
|
bytes: readBytes,
|
||||||
|
fileName: fileData.displayName ?? data.name,
|
||||||
|
contentType: actualMimetype,
|
||||||
|
client: client,
|
||||||
|
poolId: poolId,
|
||||||
|
onProgress: onProgress,
|
||||||
|
completer: completer,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catchError((e) {
|
||||||
|
completer.completeError(e);
|
||||||
|
});
|
||||||
|
return completer;
|
||||||
} else if (data is List<int> || data is Uint8List) {
|
} else if (data is List<int> || data is Uint8List) {
|
||||||
byteData = data is List<int> ? Uint8List.fromList(data) : data;
|
bytes = data is List<int> ? Uint8List.fromList(data) : data;
|
||||||
actualFilename = fileData.displayName ?? 'uploaded_file';
|
actualFilename = fileData.displayName ?? 'uploaded_file';
|
||||||
file = XFile.fromData(byteData!, mimeType: actualMimetype);
|
|
||||||
} else if (data is SnCloudFile) {
|
} else if (data is SnCloudFile) {
|
||||||
// If the file is already on the cloud, just return it
|
// If the file is already on the cloud, just return it
|
||||||
completer.complete(data);
|
completer.complete(data);
|
||||||
@@ -252,15 +250,40 @@ class FileUploader {
|
|||||||
return completer;
|
return completer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bytes != null) {
|
||||||
|
_performUpload(
|
||||||
|
bytes: bytes,
|
||||||
|
fileName: actualFilename,
|
||||||
|
contentType: actualMimetype,
|
||||||
|
client: client,
|
||||||
|
poolId: poolId,
|
||||||
|
onProgress: onProgress,
|
||||||
|
completer: completer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return completer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to perform the actual upload
|
||||||
|
static void _performUpload({
|
||||||
|
required Uint8List bytes,
|
||||||
|
required String fileName,
|
||||||
|
required String contentType,
|
||||||
|
required Dio client,
|
||||||
|
String? poolId,
|
||||||
|
Function(double progress, Duration estimate)? onProgress,
|
||||||
|
required Completer<SnCloudFile?> completer,
|
||||||
|
}) {
|
||||||
final uploader = FileUploader(client);
|
final uploader = FileUploader(client);
|
||||||
|
|
||||||
// Call progress start
|
// Call progress start
|
||||||
onProgress?.call(0.0, Duration.zero);
|
onProgress?.call(0.0, Duration.zero);
|
||||||
uploader
|
uploader
|
||||||
.uploadFile(
|
.uploadFile(
|
||||||
file: file,
|
bytes: bytes,
|
||||||
fileName: actualFilename,
|
fileName: fileName,
|
||||||
contentType: actualMimetype,
|
contentType: contentType,
|
||||||
poolId: poolId,
|
poolId: poolId,
|
||||||
)
|
)
|
||||||
.then((result) {
|
.then((result) {
|
||||||
@@ -272,8 +295,6 @@ class FileUploader {
|
|||||||
completer.completeError(e);
|
completer.completeError(e);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
|
||||||
return completer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets the MIME type of a UniversalFile.
|
/// Gets the MIME type of a UniversalFile.
|
||||||
|
|||||||
@@ -106,20 +106,6 @@ StreamSubscription<WebSocketPacket> setupNotificationListener(
|
|||||||
child: NotificationCard(notification: notification),
|
child: NotificationCard(notification: notification),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
|
||||||
if (notification.meta['action_uri'] != null) {
|
|
||||||
var uri = notification.meta['action_uri'] as String;
|
|
||||||
if (uri.startsWith('/')) {
|
|
||||||
// In-app routes
|
|
||||||
rootNavigatorKey.currentContext?.push(
|
|
||||||
notification.meta['action_uri'],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// External URLs
|
|
||||||
launchUrlString(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDismissed: () {},
|
onDismissed: () {},
|
||||||
dismissType: DismissType.onSwipe,
|
dismissType: DismissType.onSwipe,
|
||||||
displayDuration: const Duration(seconds: 5),
|
displayDuration: const Duration(seconds: 5),
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/account.dart';
|
import 'package:island/models/account.dart';
|
||||||
|
import 'package:island/route.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
class NotificationCard extends HookConsumerWidget {
|
class NotificationCard extends HookConsumerWidget {
|
||||||
final SnNotification notification;
|
final SnNotification notification;
|
||||||
@@ -14,58 +17,78 @@ class NotificationCard extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final icon = Symbols.info;
|
final icon = Symbols.info;
|
||||||
|
|
||||||
return Card(
|
return GestureDetector(
|
||||||
elevation: 4,
|
onTap: () {
|
||||||
margin: const EdgeInsets.only(bottom: 8),
|
if (notification.meta['action_uri'] != null) {
|
||||||
shape: const RoundedRectangleBorder(
|
var uri = notification.meta['action_uri'] as String;
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
if (uri.startsWith('solian://')) {
|
||||||
),
|
uri = uri.replaceFirst('solian://', '');
|
||||||
child: Column(
|
}
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
if (uri.startsWith('/')) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
// In-app routes
|
||||||
children: [
|
rootNavigatorKey.currentContext?.push(
|
||||||
Padding(
|
notification.meta['action_uri'],
|
||||||
padding: const EdgeInsets.all(12),
|
);
|
||||||
child: Row(
|
} else {
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// External URLs
|
||||||
children: [
|
launchUrlString(uri);
|
||||||
if (notification.meta['pfp'] != null)
|
}
|
||||||
ProfilePictureWidget(
|
}
|
||||||
fileId: notification.meta['pfp'],
|
},
|
||||||
radius: 12,
|
child: Card(
|
||||||
).padding(right: 12, top: 2)
|
elevation: 4,
|
||||||
else
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
Icon(
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
icon,
|
shape: const RoundedRectangleBorder(
|
||||||
color: Theme.of(context).colorScheme.primary,
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
size: 24,
|
),
|
||||||
).padding(right: 12),
|
child: Column(
|
||||||
Expanded(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
children: [
|
||||||
children: [
|
Padding(
|
||||||
Text(
|
padding: const EdgeInsets.all(12),
|
||||||
notification.title,
|
child: Row(
|
||||||
style: Theme.of(context).textTheme.titleMedium
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
?.copyWith(fontWeight: FontWeight.bold),
|
children: [
|
||||||
),
|
if (notification.meta['pfp'] != null)
|
||||||
if (notification.content.isNotEmpty)
|
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(
|
Text(
|
||||||
notification.content,
|
notification.title,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.titleMedium
|
||||||
|
?.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
if (notification.subtitle.isNotEmpty)
|
if (notification.content.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
notification.subtitle,
|
notification.content,
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
],
|
if (notification.subtitle.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
notification.subtitle,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:app_links/app_links.dart';
|
import 'dart:io';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:protocol_handler/protocol_handler.dart';
|
||||||
import 'package:island/pods/activity/activity_rpc.dart';
|
import 'package:island/pods/activity/activity_rpc.dart';
|
||||||
import 'package:island/pods/websocket.dart';
|
import 'package:island/pods/websocket.dart';
|
||||||
import 'package:island/route.dart';
|
import 'package:island/route.dart';
|
||||||
@@ -15,57 +16,61 @@ import 'package:island/widgets/tour/tour.dart';
|
|||||||
import 'package:tray_manager/tray_manager.dart';
|
import 'package:tray_manager/tray_manager.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
class AppWrapper extends HookConsumerWidget with TrayListener {
|
class AppWrapper extends ConsumerStatefulWidget {
|
||||||
final Widget child;
|
final Widget child;
|
||||||
const AppWrapper({super.key, required this.child});
|
const AppWrapper({super.key, required this.child});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<AppWrapper> createState() => _AppWrapperState();
|
||||||
useEffect(() {
|
}
|
||||||
StreamSubscription? ntySubs;
|
|
||||||
StreamSubscription? appLinksSubs;
|
|
||||||
Future(() async {
|
|
||||||
final appLinks = AppLinks();
|
|
||||||
|
|
||||||
if (context.mounted) ntySubs = setupNotificationListener(context, ref);
|
class _AppWrapperState extends ConsumerState<AppWrapper>
|
||||||
|
with ProtocolListener, TrayListener {
|
||||||
|
StreamSubscription? ntySubs;
|
||||||
|
bool networkStateShowing = false;
|
||||||
|
|
||||||
final sharingService = SharingIntentService();
|
@override
|
||||||
if (context.mounted) sharingService.initialize(context);
|
void initState() {
|
||||||
if (context.mounted) UpdateService().checkForUpdates(context);
|
super.initState();
|
||||||
|
protocolHandler.addListener(this);
|
||||||
|
Future(() async {
|
||||||
|
if (mounted) ntySubs = setupNotificationListener(context, ref);
|
||||||
|
|
||||||
TrayService.instance.initialize(this);
|
final sharingService = SharingIntentService();
|
||||||
|
if (mounted) sharingService.initialize(context);
|
||||||
|
if (mounted) UpdateService().checkForUpdates(context);
|
||||||
|
|
||||||
ref.read(rpcServerStateProvider.notifier).start();
|
TrayService.instance.initialize(this);
|
||||||
|
|
||||||
final initialUri = await appLinks.getLatestLink();
|
ref.read(rpcServerStateProvider.notifier).start();
|
||||||
if (initialUri != null && context.mounted) {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
_handleDeepLink(initialUri, ref);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
appLinksSubs = appLinks.uriLinkStream.listen((uri) {
|
final initialUrl = await protocolHandler.getInitialUrl();
|
||||||
_handleDeepLink(uri, ref);
|
if (initialUrl != null && mounted) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_handleDeepLink(Uri.parse(initialUrl), ref);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return () {
|
@override
|
||||||
ref.read(rpcServerProvider).stop();
|
void dispose() {
|
||||||
TrayService.instance.dispose(this);
|
protocolHandler.removeListener(this);
|
||||||
ntySubs?.cancel();
|
ref.read(rpcServerProvider).stop();
|
||||||
appLinksSubs?.cancel();
|
TrayService.instance.dispose(this);
|
||||||
};
|
ntySubs?.cancel();
|
||||||
}, const []);
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final wsNotifier = ref.watch(websocketStateProvider.notifier);
|
final wsNotifier = ref.watch(websocketStateProvider.notifier);
|
||||||
final websocketState = ref.watch(websocketStateProvider);
|
final websocketState = ref.watch(websocketStateProvider);
|
||||||
|
|
||||||
final networkStateShowing = useState(false);
|
|
||||||
|
|
||||||
if (websocketState == WebSocketState.duplicateDevice()) {
|
if (websocketState == WebSocketState.duplicateDevice()) {
|
||||||
if (!networkStateShowing.value) {
|
if (!networkStateShowing) {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
networkStateShowing.value = true;
|
setState(() => networkStateShowing = true);
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -73,12 +78,17 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
|
|||||||
builder:
|
builder:
|
||||||
(context) =>
|
(context) =>
|
||||||
NetworkStatusSheet(onReconnect: () => wsNotifier.connect()),
|
NetworkStatusSheet(onReconnect: () => wsNotifier.connect()),
|
||||||
).then((_) => networkStateShowing.value = false);
|
).then((_) => setState(() => networkStateShowing = false));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return TourTriggerWidget(key: UniqueKey(), child: child);
|
return TourTriggerWidget(key: UniqueKey(), child: widget.child);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onProtocolUrlReceived(String url) {
|
||||||
|
_handleDeepLink(Uri.parse(url), ref);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _trayIconPrimaryAction() {
|
void _trayIconPrimaryAction() {
|
||||||
@@ -106,13 +116,17 @@ class AppWrapper extends HookConsumerWidget with TrayListener {
|
|||||||
|
|
||||||
void _handleDeepLink(Uri uri, WidgetRef ref) {
|
void _handleDeepLink(Uri uri, WidgetRef ref) {
|
||||||
final router = ref.read(routerProvider);
|
final router = ref.read(routerProvider);
|
||||||
String path = '/${uri.path}';
|
String path = '/${uri.host}${uri.path}';
|
||||||
if (uri.queryParameters.isNotEmpty) {
|
if (uri.queryParameters.isNotEmpty) {
|
||||||
path =
|
path =
|
||||||
Uri.parse(
|
Uri.parse(
|
||||||
path,
|
path,
|
||||||
).replace(queryParameters: uri.queryParameters).toString();
|
).replace(queryParameters: uri.queryParameters).toString();
|
||||||
}
|
}
|
||||||
router.go(path);
|
router.push(path);
|
||||||
|
if (!kIsWeb &&
|
||||||
|
(Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
|
||||||
|
windowManager.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:dismissible_page/dismissible_page.dart';
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
import 'package:flutter_blurhash/flutter_blurhash.dart';
|
||||||
import 'package:file_saver/file_saver.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:gal/gal.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.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:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/file_info_sheet.dart';
|
import 'package:island/widgets/content/cloud_file_lightbox.dart';
|
||||||
import 'package:island/widgets/content/sensitive.dart';
|
import 'package:island/widgets/content/sensitive.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:path/path.dart' show extension;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
import 'package:photo_view/photo_view.dart';
|
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@@ -47,13 +38,100 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
double calculateAspectRatio() {
|
double calculateAspectRatio() {
|
||||||
double total = 0;
|
final ratios = <double>[];
|
||||||
for (var ratio in files.map((e) => e.fileMeta?['ratio'] ?? 1)) {
|
|
||||||
if (ratio is double) total += ratio;
|
// Collect all valid ratios
|
||||||
if (ratio is String) total += double.parse(ratio);
|
for (final file in files) {
|
||||||
|
final meta = file.fileMeta;
|
||||||
|
if (meta is Map<String, dynamic> && meta.containsKey('ratio')) {
|
||||||
|
final ratioValue = meta['ratio'];
|
||||||
|
if (ratioValue is num && ratioValue > 0) {
|
||||||
|
ratios.add(ratioValue.toDouble());
|
||||||
|
} else if (ratioValue is String) {
|
||||||
|
try {
|
||||||
|
final parsed = double.parse(ratioValue);
|
||||||
|
if (parsed > 0) ratios.add(parsed);
|
||||||
|
} catch (_) {
|
||||||
|
// Skip invalid string ratios
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (total == 0) return 1;
|
|
||||||
return total / files.length;
|
if (ratios.isEmpty) {
|
||||||
|
// Default to 4:3 aspect ratio when no valid ratios found
|
||||||
|
return 4 / 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratios.length == 1) {
|
||||||
|
return ratios.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group similar ratios and find the most common one
|
||||||
|
final commonRatios = <double, int>{};
|
||||||
|
|
||||||
|
// Common aspect ratios to round to (with tolerance)
|
||||||
|
const tolerance = 0.05;
|
||||||
|
final standardRatios = [
|
||||||
|
1.0,
|
||||||
|
4 / 3,
|
||||||
|
3 / 2,
|
||||||
|
16 / 9,
|
||||||
|
5 / 3,
|
||||||
|
5 / 4,
|
||||||
|
7 / 5,
|
||||||
|
9 / 16,
|
||||||
|
2 / 3,
|
||||||
|
3 / 4,
|
||||||
|
4 / 5,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final ratio in ratios) {
|
||||||
|
// Find the closest standard ratio within tolerance
|
||||||
|
double closestRatio = ratio;
|
||||||
|
double minDiff = double.infinity;
|
||||||
|
|
||||||
|
for (final standard in standardRatios) {
|
||||||
|
final diff = (ratio - standard).abs();
|
||||||
|
if (diff < minDiff && diff <= tolerance) {
|
||||||
|
minDiff = diff;
|
||||||
|
closestRatio = standard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no standard ratio is close enough, keep original
|
||||||
|
if (minDiff == double.infinity || minDiff > tolerance) {
|
||||||
|
closestRatio = ratio;
|
||||||
|
}
|
||||||
|
|
||||||
|
commonRatios[closestRatio] = (commonRatios[closestRatio] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the most frequent ratio(s)
|
||||||
|
int maxCount = 0;
|
||||||
|
final mostFrequent = <double>[];
|
||||||
|
|
||||||
|
for (final entry in commonRatios.entries) {
|
||||||
|
if (entry.value > maxCount) {
|
||||||
|
maxCount = entry.value;
|
||||||
|
mostFrequent.clear();
|
||||||
|
mostFrequent.add(entry.key);
|
||||||
|
} else if (entry.value == maxCount) {
|
||||||
|
mostFrequent.add(entry.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only one most frequent ratio, return it
|
||||||
|
if (mostFrequent.length == 1) {
|
||||||
|
return mostFrequent.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If multiple ratios have the same highest frequency, use median of them
|
||||||
|
mostFrequent.sort();
|
||||||
|
final mid = mostFrequent.length ~/ 2;
|
||||||
|
return mostFrequent.length.isEven
|
||||||
|
? (mostFrequent[mid - 1] + mostFrequent[mid]) / 2
|
||||||
|
: mostFrequent[mid];
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -90,7 +168,7 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (!disableZoomIn) {
|
if (!disableZoomIn) {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
CloudFileZoomIn(item: file, heroTag: heroTags[i]),
|
CloudFileLightbox(item: file, heroTag: heroTags[i]),
|
||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -151,7 +229,7 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (!disableZoomIn) {
|
if (!disableZoomIn) {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
CloudFileZoomIn(item: files.first, heroTag: heroTags.first),
|
CloudFileLightbox(item: files.first, heroTag: heroTags.first),
|
||||||
rootNavigator: true,
|
rootNavigator: true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -169,10 +247,7 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
child:
|
child:
|
||||||
isAudio
|
isAudio
|
||||||
? widgetItem
|
? widgetItem
|
||||||
: AspectRatio(
|
: IntrinsicWidth(child: IntrinsicHeight(child: widgetItem)),
|
||||||
aspectRatio: calculateAspectRatio(),
|
|
||||||
child: widgetItem,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,53 +263,60 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
aspectRatio: calculateAspectRatio(),
|
aspectRatio: calculateAspectRatio(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: padding ?? EdgeInsets.zero,
|
padding: padding ?? EdgeInsets.zero,
|
||||||
child: CarouselView(
|
child: LayoutBuilder(
|
||||||
itemSnapping: true,
|
builder: (context, constraints) {
|
||||||
itemExtent: math.min(
|
final availableWidth =
|
||||||
math.min(
|
constraints.maxWidth.isFinite
|
||||||
MediaQuery.of(context).size.width * 0.75,
|
? constraints.maxWidth
|
||||||
maxWidth * 0.75,
|
: MediaQuery.of(context).size.width;
|
||||||
),
|
final itemExtent = math.min(
|
||||||
640,
|
math.min(availableWidth * 0.75, maxWidth * 0.75).toDouble(),
|
||||||
),
|
640.0,
|
||||||
shape: RoundedRectangleBorder(
|
);
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
),
|
return CarouselView(
|
||||||
children: [
|
itemSnapping: true,
|
||||||
for (var i = 0; i < files.length; i++)
|
itemExtent: itemExtent,
|
||||||
Stack(
|
shape: RoundedRectangleBorder(
|
||||||
children: [
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
_CloudFileListEntry(
|
|
||||||
file: files[i],
|
|
||||||
heroTag: heroTags[i],
|
|
||||||
isImage:
|
|
||||||
files[i].mimeType?.startsWith('image') ?? false,
|
|
||||||
disableZoomIn: disableZoomIn,
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
bottom: 12,
|
|
||||||
left: 16,
|
|
||||||
child: Text('${i + 1}/${files.length}')
|
|
||||||
.textColor(Colors.white)
|
|
||||||
.textShadow(
|
|
||||||
color: Colors.black54,
|
|
||||||
offset: Offset(1, 1),
|
|
||||||
blurRadius: 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
children: [
|
||||||
onTap: (i) {
|
for (var i = 0; i < files.length; i++)
|
||||||
if (!(files[i].mimeType?.startsWith('image') ?? false)) {
|
Stack(
|
||||||
return;
|
children: [
|
||||||
}
|
_CloudFileListEntry(
|
||||||
if (!disableZoomIn) {
|
file: files[i],
|
||||||
context.pushTransparentRoute(
|
heroTag: heroTags[i],
|
||||||
CloudFileZoomIn(item: files[i], heroTag: heroTags[i]),
|
isImage:
|
||||||
rootNavigator: true,
|
files[i].mimeType?.startsWith('image') ?? false,
|
||||||
);
|
disableZoomIn: disableZoomIn,
|
||||||
}
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 12,
|
||||||
|
left: 16,
|
||||||
|
child: Text('${i + 1}/${files.length}')
|
||||||
|
.textColor(Colors.white)
|
||||||
|
.textShadow(
|
||||||
|
color: Colors.black54,
|
||||||
|
offset: Offset(1, 1),
|
||||||
|
blurRadius: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onTap: (i) {
|
||||||
|
if (!(files[i].mimeType?.startsWith('image') ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!disableZoomIn) {
|
||||||
|
context.pushTransparentRoute(
|
||||||
|
CloudFileLightbox(item: files[i], heroTag: heroTags[i]),
|
||||||
|
rootNavigator: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -273,7 +355,7 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
if (!disableZoomIn) {
|
if (!disableZoomIn) {
|
||||||
context.pushTransparentRoute(
|
context.pushTransparentRoute(
|
||||||
CloudFileZoomIn(
|
CloudFileLightbox(
|
||||||
item: files[index],
|
item: files[index],
|
||||||
heroTag: heroTags[index],
|
heroTag: heroTags[index],
|
||||||
),
|
),
|
||||||
@@ -305,211 +387,6 @@ class CloudFileList extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CloudFileZoomIn extends HookConsumerWidget {
|
|
||||||
final SnCloudFile item;
|
|
||||||
final String heroTag;
|
|
||||||
const CloudFileZoomIn({super.key, required this.item, required this.heroTag});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final serverUrl = ref.watch(serverUrlProvider);
|
|
||||||
final photoViewController = useMemoized(() => PhotoViewController(), []);
|
|
||||||
final rotation = useState(0);
|
|
||||||
|
|
||||||
final showOriginal = useState(false);
|
|
||||||
|
|
||||||
Future<void> saveToGallery() async {
|
|
||||||
try {
|
|
||||||
// Show loading indicator
|
|
||||||
showSnackBar('Saving image...');
|
|
||||||
|
|
||||||
// Get the image URL
|
|
||||||
final client = ref.watch(apiClientProvider);
|
|
||||||
|
|
||||||
// Create a temporary file to save the image
|
|
||||||
final tempDir = await getTemporaryDirectory();
|
|
||||||
var extName = extension(item.name).trim();
|
|
||||||
if (extName.isEmpty) {
|
|
||||||
extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg';
|
|
||||||
}
|
|
||||||
final filePath = '${tempDir.path}/${item.id}.$extName';
|
|
||||||
|
|
||||||
await client.download(
|
|
||||||
'/drive/files/${item.id}',
|
|
||||||
filePath,
|
|
||||||
queryParameters: {'original': true},
|
|
||||||
);
|
|
||||||
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
|
||||||
// Save to gallery
|
|
||||||
await Gal.putImage(filePath, album: 'Solar Network');
|
|
||||||
// Show success message
|
|
||||||
showSnackBar('Image saved to gallery');
|
|
||||||
} else {
|
|
||||||
await FileSaver.instance.saveFile(
|
|
||||||
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
|
||||||
file: File(filePath),
|
|
||||||
);
|
|
||||||
showSnackBar('Image saved to $filePath');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
showErrorAlert(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void showInfoSheet() {
|
|
||||||
showModalBottomSheet(
|
|
||||||
useRootNavigator: true,
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
builder: (context) => FileInfoSheet(item: item),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final shadow = [
|
|
||||||
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
|
||||||
];
|
|
||||||
|
|
||||||
return DismissiblePage(
|
|
||||||
isFullScreen: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
direction: DismissiblePageDismissDirection.down,
|
|
||||||
onDismissed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
Positioned.fill(
|
|
||||||
child: PhotoView(
|
|
||||||
backgroundDecoration: BoxDecoration(
|
|
||||||
color: Colors.black.withOpacity(0.9),
|
|
||||||
),
|
|
||||||
controller: photoViewController,
|
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: heroTag),
|
|
||||||
imageProvider: CloudImageWidget.provider(
|
|
||||||
fileId: item.id,
|
|
||||||
serverUrl: serverUrl,
|
|
||||||
original: showOriginal.value,
|
|
||||||
),
|
|
||||||
// Apply rotation transformation
|
|
||||||
customSize: MediaQuery.of(context).size,
|
|
||||||
basePosition: Alignment.center,
|
|
||||||
filterQuality: FilterQuality.high,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Close button and save button
|
|
||||||
Positioned(
|
|
||||||
top: MediaQuery.of(context).padding.top + 16,
|
|
||||||
right: 16,
|
|
||||||
left: 16,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
if (!kIsWeb)
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.save_alt,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: shadow,
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
saveToGallery();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
showOriginal.value = !showOriginal.value;
|
|
||||||
},
|
|
||||||
icon: Icon(
|
|
||||||
showOriginal.value ? Symbols.hd : Symbols.sd,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: shadow,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.close, color: Colors.white, shadows: shadow),
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Rotation controls
|
|
||||||
Positioned(
|
|
||||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
|
||||||
left: 16,
|
|
||||||
right: 16,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.info_outline,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: shadow,
|
|
||||||
),
|
|
||||||
onPressed: showInfoSheet,
|
|
||||||
),
|
|
||||||
Spacer(),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.remove,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: shadow,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
photoViewController.scale =
|
|
||||||
(photoViewController.scale ?? 1) - 0.05;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.add, color: Colors.white, shadows: shadow),
|
|
||||||
onPressed: () {
|
|
||||||
photoViewController.scale =
|
|
||||||
(photoViewController.scale ?? 1) + 0.05;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Gap(8),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.rotate_left,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: shadow,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
rotation.value = (rotation.value - 1) % 4;
|
|
||||||
photoViewController.rotation =
|
|
||||||
rotation.value * -math.pi / 2;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.rotate_right,
|
|
||||||
color: Colors.white,
|
|
||||||
shadows: [
|
|
||||||
Shadow(
|
|
||||||
color: Colors.black54,
|
|
||||||
blurRadius: 5.0,
|
|
||||||
offset: Offset(1.0, 1.0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
rotation.value = (rotation.value + 1) % 4;
|
|
||||||
photoViewController.rotation =
|
|
||||||
rotation.value * -math.pi / 2;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CloudFileListEntry extends HookConsumerWidget {
|
class _CloudFileListEntry extends HookConsumerWidget {
|
||||||
final SnCloudFile file;
|
final SnCloudFile file;
|
||||||
final String heroTag;
|
final String heroTag;
|
||||||
@@ -535,15 +412,8 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
final lockedByDS = dataSaving && !showDataSaving.value;
|
final lockedByDS = dataSaving && !showDataSaving.value;
|
||||||
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
final lockedByMature = file.sensitiveMarks.isNotEmpty && !showMature.value;
|
||||||
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
final meta = file.fileMeta is Map ? file.fileMeta as Map : const {};
|
||||||
final hasRatio =
|
|
||||||
meta.containsKey('ratio') &&
|
|
||||||
(meta['ratio'] is num && (meta['ratio'] as num) != 0);
|
|
||||||
final ratio =
|
|
||||||
(meta['ratio'] is num && (meta['ratio'] as num) != 0)
|
|
||||||
? (meta['ratio'] as num).toDouble()
|
|
||||||
: 1.0;
|
|
||||||
|
|
||||||
final fit = hasRatio ? BoxFit.cover : BoxFit.contain;
|
final fit = BoxFit.cover;
|
||||||
|
|
||||||
Widget bg = const SizedBox.shrink();
|
Widget bg = const SizedBox.shrink();
|
||||||
if (isImage) {
|
if (isImage) {
|
||||||
@@ -551,7 +421,7 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
bg = BlurHash(hash: meta['blur'] as String);
|
bg = BlurHash(hash: meta['blur'] as String);
|
||||||
} else if (!lockedByDS && !lockedByMature) {
|
} else if (!lockedByDS && !lockedByMature) {
|
||||||
bg = ImageFiltered(
|
bg = ImageFiltered(
|
||||||
imageFilter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
|
imageFilter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||||
child: CloudFileWidget(
|
child: CloudFileWidget(
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
item: file,
|
item: file,
|
||||||
@@ -581,7 +451,9 @@ class _CloudFileListEntry extends HookConsumerWidget {
|
|||||||
fit: fit,
|
fit: fit,
|
||||||
useInternalGate: false,
|
useInternalGate: false,
|
||||||
))
|
))
|
||||||
: AspectRatio(aspectRatio: ratio, child: const SizedBox.shrink());
|
: IntrinsicWidth(
|
||||||
|
child: IntrinsicHeight(child: const SizedBox.shrink()),
|
||||||
|
);
|
||||||
|
|
||||||
Widget overlays;
|
Widget overlays;
|
||||||
if (lockedByDS) {
|
if (lockedByDS) {
|
||||||
|
|||||||
231
lib/widgets/content/cloud_file_lightbox.dart
Normal file
231
lib/widgets/content/cloud_file_lightbox.dart
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:dismissible_page/dismissible_page.dart';
|
||||||
|
import 'package:file_saver/file_saver.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:gal/gal.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
|
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/file_info_sheet.dart';
|
||||||
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
|
import 'package:path/path.dart' show extension;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
|
||||||
|
import 'cloud_files.dart';
|
||||||
|
|
||||||
|
class CloudFileLightbox extends HookConsumerWidget {
|
||||||
|
final SnCloudFile item;
|
||||||
|
final String heroTag;
|
||||||
|
const CloudFileLightbox({
|
||||||
|
super.key,
|
||||||
|
required this.item,
|
||||||
|
required this.heroTag,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final serverUrl = ref.watch(serverUrlProvider);
|
||||||
|
final photoViewController = useMemoized(() => PhotoViewController(), []);
|
||||||
|
final rotation = useState(0);
|
||||||
|
|
||||||
|
final showOriginal = useState(false);
|
||||||
|
|
||||||
|
Future<void> saveToGallery() async {
|
||||||
|
try {
|
||||||
|
// Show loading indicator
|
||||||
|
showSnackBar('Saving image...');
|
||||||
|
|
||||||
|
// Get the image URL
|
||||||
|
final client = ref.watch(apiClientProvider);
|
||||||
|
|
||||||
|
// Create a temporary file to save the image
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
var extName = extension(item.name).trim();
|
||||||
|
if (extName.isEmpty) {
|
||||||
|
extName = item.mimeType?.split('/').lastOrNull ?? 'jpeg';
|
||||||
|
}
|
||||||
|
final filePath = '${tempDir.path}/${item.id}.$extName';
|
||||||
|
|
||||||
|
await client.download(
|
||||||
|
'/drive/files/${item.id}',
|
||||||
|
filePath,
|
||||||
|
queryParameters: {'original': true},
|
||||||
|
);
|
||||||
|
if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
|
||||||
|
// Save to gallery
|
||||||
|
await Gal.putImage(filePath, album: 'Solar Network');
|
||||||
|
// Show success message
|
||||||
|
showSnackBar('Image saved to gallery');
|
||||||
|
} else {
|
||||||
|
await FileSaver.instance.saveFile(
|
||||||
|
name: item.name.isEmpty ? '${item.id}.$extName' : item.name,
|
||||||
|
file: File(filePath),
|
||||||
|
);
|
||||||
|
showSnackBar('Image saved to $filePath');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showErrorAlert(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showInfoSheet() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
useRootNavigator: true,
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) => FileInfoSheet(item: item),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final shadow = [
|
||||||
|
Shadow(color: Colors.black54, blurRadius: 5.0, offset: Offset(1.0, 1.0)),
|
||||||
|
];
|
||||||
|
|
||||||
|
return DismissiblePage(
|
||||||
|
isFullScreen: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
direction: DismissiblePageDismissDirection.down,
|
||||||
|
onDismissed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: PhotoView(
|
||||||
|
backgroundDecoration: BoxDecoration(
|
||||||
|
color: Colors.black.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
controller: photoViewController,
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(tag: heroTag),
|
||||||
|
imageProvider: CloudImageWidget.provider(
|
||||||
|
fileId: item.id,
|
||||||
|
serverUrl: serverUrl,
|
||||||
|
original: showOriginal.value,
|
||||||
|
),
|
||||||
|
// Apply rotation transformation
|
||||||
|
customSize: MediaQuery.of(context).size,
|
||||||
|
basePosition: Alignment.center,
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Close button and save button
|
||||||
|
Positioned(
|
||||||
|
top: MediaQuery.of(context).padding.top + 16,
|
||||||
|
right: 16,
|
||||||
|
left: 16,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (!kIsWeb)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.save_alt,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
saveToGallery();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
showOriginal.value = !showOriginal.value;
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
showOriginal.value ? Symbols.hd : Symbols.sd,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.close, color: Colors.white, shadows: shadow),
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Rotation controls
|
||||||
|
Positioned(
|
||||||
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
|
left: 16,
|
||||||
|
right: 16,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
onPressed: showInfoSheet,
|
||||||
|
),
|
||||||
|
Spacer(),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.remove,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
photoViewController.scale =
|
||||||
|
(photoViewController.scale ?? 1) - 0.05;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.add, color: Colors.white, shadows: shadow),
|
||||||
|
onPressed: () {
|
||||||
|
photoViewController.scale =
|
||||||
|
(photoViewController.scale ?? 1) + 0.05;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Gap(8),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.rotate_left,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: shadow,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
rotation.value = (rotation.value - 1) % 4;
|
||||||
|
photoViewController.rotation =
|
||||||
|
rotation.value * -math.pi / 2;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.rotate_right,
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
color: Colors.black54,
|
||||||
|
blurRadius: 5.0,
|
||||||
|
offset: Offset(1.0, 1.0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
rotation.value = (rotation.value + 1) % 4;
|
||||||
|
photoViewController.rotation =
|
||||||
|
rotation.value * -math.pi / 2;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -371,13 +371,21 @@ class CloudFileWidget extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var content = switch (item.mimeType?.split('/').firstOrNull) {
|
var content = switch (item.mimeType?.split('/').firstOrNull) {
|
||||||
'image' => AspectRatio(
|
'image' =>
|
||||||
aspectRatio: ratio,
|
ratio == 1.0
|
||||||
child:
|
? IntrinsicHeight(
|
||||||
(useInternalGate && dataSaving && !unlocked.value)
|
child:
|
||||||
? dataPlaceHolder(Symbols.image)
|
(useInternalGate && dataSaving && !unlocked.value)
|
||||||
: cloudImage(),
|
? dataPlaceHolder(Symbols.image)
|
||||||
),
|
: cloudImage(),
|
||||||
|
)
|
||||||
|
: AspectRatio(
|
||||||
|
aspectRatio: ratio,
|
||||||
|
child:
|
||||||
|
(useInternalGate && dataSaving && !unlocked.value)
|
||||||
|
? dataPlaceHolder(Symbols.image)
|
||||||
|
: cloudImage(),
|
||||||
|
),
|
||||||
'video' => AspectRatio(
|
'video' => AspectRatio(
|
||||||
aspectRatio: ratio,
|
aspectRatio: ratio,
|
||||||
child:
|
child:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:island/models/embed.dart';
|
import 'package:island/models/embed.dart';
|
||||||
import 'package:island/models/poll.dart';
|
|
||||||
import 'package:island/services/responsive.dart';
|
import 'package:island/services/responsive.dart';
|
||||||
import 'package:island/utils/mapping.dart';
|
import 'package:island/utils/mapping.dart';
|
||||||
import 'package:island/widgets/content/embed/link.dart';
|
import 'package:island/widgets/content/embed/link.dart';
|
||||||
@@ -54,13 +53,10 @@ class EmbedListWidget extends StatelessWidget {
|
|||||||
vertical: 8,
|
vertical: 8,
|
||||||
),
|
),
|
||||||
child:
|
child:
|
||||||
embedData['poll'] == null
|
embedData['id'] == null
|
||||||
? const Text('Poll was not loaded...')
|
? const Text('Poll was unavailable...')
|
||||||
: PollSubmit(
|
: PollSubmit(
|
||||||
initialAnswers:
|
pollId: embedData['id'],
|
||||||
embedData['poll']?['user_answer']?['answer'],
|
|
||||||
stats: embedData['poll']?['stats'],
|
|
||||||
poll: SnPollWithStats.fromJson(embedData['poll']),
|
|
||||||
onSubmit: (_) {},
|
onSubmit: (_) {},
|
||||||
isReadonly: !isInteractive,
|
isReadonly: !isInteractive,
|
||||||
isInitiallyExpanded: isFullPost,
|
isInitiallyExpanded: isFullPost,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:island/models/file.dart';
|
import 'package:island/models/file.dart';
|
||||||
import 'package:island/pods/config.dart';
|
import 'package:island/pods/config.dart';
|
||||||
|
import 'package:island/screens/account/profile.dart';
|
||||||
|
import 'package:island/screens/creators/publishers_form.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/content/cloud_files.dart';
|
import 'package:island/widgets/content/cloud_files.dart';
|
||||||
import 'package:island/widgets/content/markdown_latex.dart';
|
import 'package:island/widgets/content/markdown_latex.dart';
|
||||||
@@ -397,7 +399,13 @@ class MentionChipSpanNode extends SpanNode {
|
|||||||
onTap: () => onTap(type, id),
|
onTap: () => onTap(type, id),
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
padding: const EdgeInsets.only(
|
||||||
|
left: 5,
|
||||||
|
right: 7,
|
||||||
|
top: 2.5,
|
||||||
|
bottom: 2.5,
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor.withOpacity(0.1),
|
color: backgroundColor.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
@@ -411,18 +419,58 @@ class MentionChipSpanNode extends SpanNode {
|
|||||||
color: backgroundColor.withOpacity(0.5),
|
color: backgroundColor.withOpacity(0.5),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(32)),
|
borderRadius: const BorderRadius.all(Radius.circular(32)),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: switch (parts.length == 1 ? 'u' : parts.first) {
|
||||||
switch (parts.first.isEmpty ? 'u' : parts.first) {
|
'u' => Consumer(
|
||||||
'c' => Symbols.forum_rounded,
|
builder: (context, ref, _) {
|
||||||
'r' => Symbols.group_rounded,
|
final userData = ref.watch(accountProvider(parts.last));
|
||||||
'u' => Symbols.person_rounded,
|
return userData.when(
|
||||||
'p' => Symbols.edit_rounded,
|
data:
|
||||||
_ => Symbols.person_rounded,
|
(data) => ProfilePictureWidget(
|
||||||
},
|
file: data.profile.picture,
|
||||||
size: 14,
|
fallbackIcon: Symbols.person_rounded,
|
||||||
color: foregroundColor,
|
radius: 9,
|
||||||
fill: 1,
|
),
|
||||||
).padding(all: 2),
|
error: (_, _) => const Icon(Symbols.close),
|
||||||
|
loading:
|
||||||
|
() => const SizedBox(
|
||||||
|
width: 9,
|
||||||
|
height: 9,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'p' => Consumer(
|
||||||
|
builder: (context, ref, _) {
|
||||||
|
final pubData = ref.watch(publisherProvider(parts.last));
|
||||||
|
return pubData.when(
|
||||||
|
data:
|
||||||
|
(data) => ProfilePictureWidget(
|
||||||
|
file: data?.picture,
|
||||||
|
fallbackIcon: Symbols.design_services_rounded,
|
||||||
|
radius: 9,
|
||||||
|
),
|
||||||
|
error: (_, _) => const Icon(Symbols.close),
|
||||||
|
loading:
|
||||||
|
() => const SizedBox(
|
||||||
|
width: 9,
|
||||||
|
height: 9,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_ => Icon(
|
||||||
|
(switch (parts.length == 1 ? 'u' : parts.first) {
|
||||||
|
'c' => Symbols.forum_rounded,
|
||||||
|
'r' => Symbols.group_rounded,
|
||||||
|
_ => Symbols.person_rounded,
|
||||||
|
}),
|
||||||
|
size: 14,
|
||||||
|
color: foregroundColor,
|
||||||
|
fill: 1,
|
||||||
|
).padding(all: 2),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
parts.last,
|
parts.last,
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:island/models/poll.dart';
|
import 'package:island/models/poll.dart';
|
||||||
import 'package:island/pods/network.dart';
|
import 'package:island/pods/network.dart';
|
||||||
|
import 'package:island/screens/creators/poll/poll_list.dart';
|
||||||
import 'package:island/widgets/alert.dart';
|
import 'package:island/widgets/alert.dart';
|
||||||
import 'package:island/widgets/poll/poll_stats_widget.dart';
|
import 'package:island/widgets/poll/poll_stats_widget.dart';
|
||||||
|
|
||||||
class PollSubmit extends ConsumerStatefulWidget {
|
class PollSubmit extends ConsumerStatefulWidget {
|
||||||
const PollSubmit({
|
const PollSubmit({
|
||||||
super.key,
|
super.key,
|
||||||
required this.poll,
|
required this.pollId,
|
||||||
required this.onSubmit,
|
required this.onSubmit,
|
||||||
required this.stats,
|
|
||||||
this.initialAnswers,
|
this.initialAnswers,
|
||||||
this.onCancel,
|
this.onCancel,
|
||||||
this.showProgress = true,
|
this.showProgress = true,
|
||||||
@@ -20,14 +20,13 @@ class PollSubmit extends ConsumerStatefulWidget {
|
|||||||
this.isInitiallyExpanded = false,
|
this.isInitiallyExpanded = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final SnPollWithStats poll;
|
final String pollId;
|
||||||
|
|
||||||
/// Callback when user submits all answers. Map questionId -> answer.
|
/// Callback when user submits all answers. Map questionId -> answer.
|
||||||
final void Function(Map<String, dynamic> answers) onSubmit;
|
final void Function(Map<String, dynamic> answers) onSubmit;
|
||||||
|
|
||||||
/// Optional initial answers, keyed by questionId.
|
/// Optional initial answers, keyed by questionId.
|
||||||
final Map<String, dynamic>? initialAnswers;
|
final Map<String, dynamic>? initialAnswers;
|
||||||
final Map<String, dynamic>? stats;
|
|
||||||
|
|
||||||
/// Optional cancel callback.
|
/// Optional cancel callback.
|
||||||
final VoidCallback? onCancel;
|
final VoidCallback? onCancel;
|
||||||
@@ -45,7 +44,7 @@ class PollSubmit extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PollSubmitState extends ConsumerState<PollSubmit> {
|
class _PollSubmitState extends ConsumerState<PollSubmit> {
|
||||||
late final List<SnPollQuestion> _questions;
|
List<SnPollQuestion>? _questions;
|
||||||
int _index = 0;
|
int _index = 0;
|
||||||
bool _submitting = false;
|
bool _submitting = false;
|
||||||
bool _isModifying = false; // New state to track if user is modifying answers
|
bool _isModifying = false; // New state to track if user is modifying answers
|
||||||
@@ -66,14 +65,10 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Ensure questions are ordered by `order`
|
|
||||||
_questions = [...widget.poll.questions]
|
|
||||||
..sort((a, b) => a.order.compareTo(b.order));
|
|
||||||
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
||||||
// Set initial collapse state based on the parameter
|
// Set initial collapse state based on the parameter
|
||||||
_isCollapsed = !widget.isInitiallyExpanded;
|
_isCollapsed = !widget.isInitiallyExpanded;
|
||||||
if (!widget.isReadonly) {
|
if (!widget.isReadonly) {
|
||||||
_loadCurrentIntoLocalState();
|
|
||||||
// If initial answers are provided, set _isModifying to false initially
|
// If initial answers are provided, set _isModifying to false initially
|
||||||
// so the "Modify" button is shown.
|
// so the "Modify" button is shown.
|
||||||
if (widget.initialAnswers != null && widget.initialAnswers!.isNotEmpty) {
|
if (widget.initialAnswers != null && widget.initialAnswers!.isNotEmpty) {
|
||||||
@@ -82,23 +77,25 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _initializeFromPollData(SnPollWithStats poll) {
|
||||||
|
// Initialize answers from poll data if available
|
||||||
|
if (poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty) {
|
||||||
|
_answers = Map<String, dynamic>.from(poll.userAnswer!.answer);
|
||||||
|
if (!widget.isReadonly && !_isModifying) {
|
||||||
|
_isModifying = false; // Show modify button if user has answered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_loadCurrentIntoLocalState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(covariant PollSubmit oldWidget) {
|
void didUpdateWidget(covariant PollSubmit oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
if (oldWidget.poll.id != widget.poll.id) {
|
if (oldWidget.pollId != widget.pollId) {
|
||||||
_index = 0;
|
_index = 0;
|
||||||
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
_answers = Map<String, dynamic>.from(widget.initialAnswers ?? {});
|
||||||
_questions
|
// Reset modification state when poll changes
|
||||||
..clear()
|
_isModifying = false;
|
||||||
..addAll(
|
|
||||||
[...widget.poll.questions]
|
|
||||||
..sort((a, b) => a.order.compareTo(b.order)),
|
|
||||||
);
|
|
||||||
if (!widget.isReadonly) {
|
|
||||||
_loadCurrentIntoLocalState();
|
|
||||||
// If poll ID changes, reset modification state
|
|
||||||
_isModifying = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +105,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
SnPollQuestion get _current => _questions[_index];
|
SnPollQuestion get _current => _questions![_index];
|
||||||
|
|
||||||
void _loadCurrentIntoLocalState() {
|
void _loadCurrentIntoLocalState() {
|
||||||
final q = _current;
|
final q = _current;
|
||||||
@@ -201,7 +198,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _submitToServer() async {
|
Future<void> _submitToServer(SnPollWithStats poll) async {
|
||||||
// Persist current question before final submit
|
// Persist current question before final submit
|
||||||
_persistCurrentAnswer();
|
_persistCurrentAnswer();
|
||||||
|
|
||||||
@@ -213,7 +210,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
final dio = ref.read(apiClientProvider);
|
final dio = ref.read(apiClientProvider);
|
||||||
|
|
||||||
await dio.post(
|
await dio.post(
|
||||||
'/sphere/polls/${widget.poll.id}/answer',
|
'/sphere/polls/${poll.id}/answer',
|
||||||
data: {'answer': _answers},
|
data: {'answer': _answers},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -233,17 +230,17 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _next() {
|
void _next(SnPollWithStats poll) {
|
||||||
if (_submitting) return;
|
if (_submitting) return;
|
||||||
_persistCurrentAnswer();
|
_persistCurrentAnswer();
|
||||||
if (_index < _questions.length - 1) {
|
if (_index < _questions!.length - 1) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_index++;
|
_index++;
|
||||||
_loadCurrentIntoLocalState();
|
_loadCurrentIntoLocalState();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Final submit to API
|
// Final submit to API
|
||||||
_submitToServer();
|
_submitToServer(poll);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,41 +258,15 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHeader(BuildContext context) {
|
Widget _buildHeader(BuildContext context, SnPollWithStats poll) {
|
||||||
final q = _current;
|
final q = _current;
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null || widget.poll.description != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (widget.poll.title != null)
|
|
||||||
Text(
|
|
||||||
widget.poll.title!,
|
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
|
||||||
),
|
|
||||||
if (widget.poll.description != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 4),
|
|
||||||
child: Text(
|
|
||||||
widget.poll.description!,
|
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodyMedium?.color?.withOpacity(0.7),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (widget.showProgress &&
|
if (widget.showProgress &&
|
||||||
_isModifying) // Only show progress when modifying
|
_isModifying) // Only show progress when modifying
|
||||||
Text(
|
Text(
|
||||||
'${_index + 1} / ${_questions.length}',
|
'${_index + 1} / ${_questions!.length}',
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -334,12 +305,18 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStats(BuildContext context, SnPollQuestion q) {
|
Widget _buildStats(
|
||||||
return PollStatsWidget(question: q, stats: widget.stats);
|
BuildContext context,
|
||||||
|
SnPollQuestion q,
|
||||||
|
Map<String, dynamic>? stats,
|
||||||
|
) {
|
||||||
|
return PollStatsWidget(question: q, stats: stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBody(BuildContext context) {
|
Widget _buildBody(BuildContext context, SnPollWithStats poll) {
|
||||||
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
|
final hasUserAnswer =
|
||||||
|
poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty;
|
||||||
|
if (hasUserAnswer && !widget.isReadonly && !_isModifying) {
|
||||||
return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying
|
return const SizedBox.shrink(); // Collapse input fields if already submitted and not modifying
|
||||||
}
|
}
|
||||||
final q = _current;
|
final q = _current;
|
||||||
@@ -449,11 +426,13 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNavBar(BuildContext context) {
|
Widget _buildNavBar(BuildContext context, SnPollWithStats poll) {
|
||||||
final isLast = _index == _questions.length - 1;
|
final isLast = _index == _questions!.length - 1;
|
||||||
final canProceed = _isCurrentAnswered() && !_submitting;
|
final canProceed = _isCurrentAnswered() && !_submitting;
|
||||||
|
final hasUserAnswer =
|
||||||
|
poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty;
|
||||||
|
|
||||||
if (widget.initialAnswers != null && !_isModifying && !widget.isReadonly) {
|
if (hasUserAnswer && !_isModifying && !widget.isReadonly) {
|
||||||
// If poll is submitted and not in modification mode, show "Modify" button
|
// If poll is submitted and not in modification mode, show "Modify" button
|
||||||
return FilledButton.icon(
|
return FilledButton.icon(
|
||||||
icon: const Icon(Icons.edit),
|
icon: const Icon(Icons.edit),
|
||||||
@@ -498,32 +477,32 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
)
|
)
|
||||||
: Icon(isLast ? Icons.check : Icons.arrow_forward),
|
: Icon(isLast ? Icons.check : Icons.arrow_forward),
|
||||||
label: Text(isLast ? 'submit'.tr() : 'next'.tr()),
|
label: Text(isLast ? 'submit'.tr() : 'next'.tr()),
|
||||||
onPressed: canProceed ? _next : null,
|
onPressed: canProceed ? () => _next(poll) : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSubmittedView(BuildContext context) {
|
Widget _buildSubmittedView(BuildContext context, SnPollWithStats poll) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null || widget.poll.description != null)
|
if (poll.title != null || poll.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title?.isNotEmpty ?? false)
|
if (poll.title?.isNotEmpty ?? false)
|
||||||
Text(
|
Text(
|
||||||
widget.poll.title!,
|
poll.title!,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
if (widget.poll.description?.isNotEmpty ?? false)
|
if (poll.description?.isNotEmpty ?? false)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.poll.description!,
|
poll.description!,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -534,7 +513,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (final q in _questions)
|
for (final q in _questions!)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -574,7 +553,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildStats(context, q),
|
_buildStats(context, q, poll.stats),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -582,26 +561,26 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReadonlyView(BuildContext context) {
|
Widget _buildReadonlyView(BuildContext context, SnPollWithStats poll) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null || widget.poll.description != null)
|
if (poll.title != null || poll.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null)
|
if (poll.title != null)
|
||||||
Text(
|
Text(
|
||||||
widget.poll.title!,
|
poll.title!,
|
||||||
style: Theme.of(context).textTheme.titleLarge,
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
),
|
),
|
||||||
if (widget.poll.description != null)
|
if (poll.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 4),
|
padding: const EdgeInsets.only(top: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.poll.description!,
|
poll.description!,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -612,7 +591,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (final q in _questions)
|
for (final q in _questions!)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -652,7 +631,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildStats(context, q),
|
_buildStats(context, q, poll.stats),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -660,7 +639,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCollapsedView(BuildContext context) {
|
Widget _buildCollapsedView(BuildContext context, SnPollWithStats poll) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@@ -670,20 +649,20 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (widget.poll.title != null)
|
if (poll.title != null)
|
||||||
Text(
|
Text(
|
||||||
widget.poll.title!,
|
poll.title!,
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
if (widget.poll.description != null)
|
if (poll.description != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.poll.description!,
|
poll.description!,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -697,7 +676,7 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 2),
|
padding: const EdgeInsets.only(top: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${_questions.length} question${_questions.length == 1 ? '' : 's'}',
|
'${_questions!.length} question${_questions!.length == 1 ? '' : 's'}',
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
@@ -729,111 +708,156 @@ class _PollSubmitState extends ConsumerState<PollSubmit> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_questions.isEmpty) {
|
final pollAsync = ref.watch(pollWithStatsProvider(widget.pollId));
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If collapsed, show collapsed view for all states
|
return pollAsync.when(
|
||||||
if (_isCollapsed) {
|
loading:
|
||||||
return _buildCollapsedView(context);
|
() => const Center(
|
||||||
}
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
// If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view
|
child: CircularProgressIndicator(),
|
||||||
if (widget.initialAnswers != null && !widget.isReadonly && !_isModifying) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
_buildCollapsedView(context),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, anim) {
|
|
||||||
final offset = Tween<Offset>(
|
|
||||||
begin: const Offset(0, -0.1),
|
|
||||||
end: Offset.zero,
|
|
||||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
|
||||||
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
|
||||||
return FadeTransition(
|
|
||||||
opacity: fade,
|
|
||||||
child: SlideTransition(position: offset, child: child),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Column(
|
|
||||||
key: const ValueKey('submitted_expanded'),
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [_buildSubmittedView(context), _buildNavBar(context)],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
error:
|
||||||
);
|
(error, stack) => Center(
|
||||||
}
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
// If poll is in readonly mode, show readonly view
|
child: Text('Failed to load poll: $error'),
|
||||||
if (widget.isReadonly) {
|
),
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
_buildCollapsedView(context),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
AnimatedSwitcher(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, anim) {
|
|
||||||
final offset = Tween<Offset>(
|
|
||||||
begin: const Offset(0, -0.1),
|
|
||||||
end: Offset.zero,
|
|
||||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
|
||||||
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
|
||||||
return FadeTransition(
|
|
||||||
opacity: fade,
|
|
||||||
child: SlideTransition(position: offset, child: child),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: _buildReadonlyView(context),
|
|
||||||
),
|
),
|
||||||
],
|
data: (poll) {
|
||||||
);
|
// Initialize questions when data is available
|
||||||
}
|
_questions = [...poll.questions]
|
||||||
|
..sort((a, b) => a.order.compareTo(b.order));
|
||||||
|
|
||||||
return Column(
|
// Initialize answers from poll data
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
_initializeFromPollData(poll);
|
||||||
children: [
|
|
||||||
_buildCollapsedView(context),
|
if (_questions!.isEmpty) {
|
||||||
const SizedBox(height: 8),
|
return const SizedBox.shrink();
|
||||||
AnimatedSwitcher(
|
}
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
transitionBuilder: (child, anim) {
|
// If collapsed, show collapsed view for all states
|
||||||
final offset = Tween<Offset>(
|
if (_isCollapsed) {
|
||||||
begin: const Offset(0, -0.1),
|
return _buildCollapsedView(context, poll);
|
||||||
end: Offset.zero,
|
}
|
||||||
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
|
||||||
final fade = CurvedAnimation(parent: anim, curve: Curves.easeOut);
|
// If poll is already submitted and not in readonly mode, and not in modification mode, show submitted view
|
||||||
return FadeTransition(
|
final hasUserAnswer =
|
||||||
opacity: fade,
|
poll.userAnswer != null && poll.userAnswer!.answer.isNotEmpty;
|
||||||
child: SlideTransition(position: offset, child: child),
|
if (hasUserAnswer && !widget.isReadonly && !_isModifying) {
|
||||||
);
|
return Column(
|
||||||
},
|
|
||||||
child: Column(
|
|
||||||
key: const ValueKey('normal_expanded'),
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildHeader(context),
|
_buildCollapsedView(context, poll),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 8),
|
||||||
_AnimatedStep(
|
AnimatedSwitcher(
|
||||||
key: ValueKey(_current.id),
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, anim) {
|
||||||
|
final offset = Tween<Offset>(
|
||||||
|
begin: const Offset(0, -0.1),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(parent: anim, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
final fade = CurvedAnimation(
|
||||||
|
parent: anim,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: fade,
|
||||||
|
child: SlideTransition(position: offset, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
key: const ValueKey('submitted_expanded'),
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_buildBody(context),
|
_buildSubmittedView(context, poll),
|
||||||
_buildStats(context, _current),
|
_buildNavBar(context, poll),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildNavBar(context),
|
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
),
|
}
|
||||||
],
|
|
||||||
|
// If poll is in readonly mode, show readonly view
|
||||||
|
if (widget.isReadonly) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildCollapsedView(context, poll),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, anim) {
|
||||||
|
final offset = Tween<Offset>(
|
||||||
|
begin: const Offset(0, -0.1),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(
|
||||||
|
CurvedAnimation(parent: anim, curve: Curves.easeOut),
|
||||||
|
);
|
||||||
|
final fade = CurvedAnimation(
|
||||||
|
parent: anim,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: fade,
|
||||||
|
child: SlideTransition(position: offset, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: _buildReadonlyView(context, poll),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildCollapsedView(context, poll),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
transitionBuilder: (child, anim) {
|
||||||
|
final offset = Tween<Offset>(
|
||||||
|
begin: const Offset(0, -0.1),
|
||||||
|
end: Offset.zero,
|
||||||
|
).animate(CurvedAnimation(parent: anim, curve: Curves.easeOut));
|
||||||
|
final fade = CurvedAnimation(
|
||||||
|
parent: anim,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: fade,
|
||||||
|
child: SlideTransition(position: offset, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
key: const ValueKey('normal_expanded'),
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildHeader(context, poll),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_AnimatedStep(
|
||||||
|
key: ValueKey(_current.id),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_buildBody(context, poll),
|
||||||
|
_buildStats(context, _current, poll.stats),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildNavBar(context, poll),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ class PostComposeCard extends HookConsumerWidget {
|
|||||||
state: composeState,
|
state: composeState,
|
||||||
originalPost: originalPost,
|
originalPost: originalPost,
|
||||||
isCompact: true,
|
isCompact: true,
|
||||||
|
useSafeArea: isContained,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
final ComposeState state;
|
final ComposeState state;
|
||||||
final SnPost? originalPost;
|
final SnPost? originalPost;
|
||||||
final bool isCompact;
|
final bool isCompact;
|
||||||
|
final bool useSafeArea;
|
||||||
|
|
||||||
const ComposeToolbar({
|
const ComposeToolbar({
|
||||||
super.key,
|
super.key,
|
||||||
required this.state,
|
required this.state,
|
||||||
this.originalPost,
|
this.originalPost,
|
||||||
this.isCompact = false,
|
this.isCompact = false,
|
||||||
|
this.useSafeArea = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -200,7 +202,12 @@ class ComposeToolbar extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 8, vertical: 4),
|
).padding(
|
||||||
|
horizontal: 8,
|
||||||
|
top: 4,
|
||||||
|
bottom:
|
||||||
|
useSafeArea ? MediaQuery.of(context).padding.bottom + 4 : 4,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ class PostListNotifier extends _$PostListNotifier
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) {
|
}) {
|
||||||
return fetch(cursor: null);
|
return fetch(cursor: null);
|
||||||
}
|
}
|
||||||
@@ -36,14 +42,20 @@ class PostListNotifier extends _$PostListNotifier
|
|||||||
final queryParams = {
|
final queryParams = {
|
||||||
'offset': offset,
|
'offset': offset,
|
||||||
'take': _pageSize,
|
'take': _pageSize,
|
||||||
|
'replies': includeReplies,
|
||||||
|
'orderDesc': orderDesc,
|
||||||
|
if (shuffle) 'shuffle': shuffle,
|
||||||
if (pubName != null) 'pub': pubName,
|
if (pubName != null) 'pub': pubName,
|
||||||
if (realm != null) 'realm': realm,
|
if (realm != null) 'realm': realm,
|
||||||
if (type != null) 'type': type,
|
if (type != null) 'type': type,
|
||||||
if (tags != null) 'tags': tags,
|
if (tags != null) 'tags': tags,
|
||||||
if (categories != null) 'categories': categories,
|
if (categories != null) 'categories': categories,
|
||||||
if (shuffle) 'shuffle': true,
|
|
||||||
if (pinned != null) 'pinned': pinned,
|
if (pinned != null) 'pinned': pinned,
|
||||||
if (includeReplies != null) 'includeReplies': includeReplies,
|
if (order != null) 'order': order,
|
||||||
|
if (periodStart != null) 'periodStart': periodStart,
|
||||||
|
if (periodEnd != null) 'periodEnd': periodEnd,
|
||||||
|
if (queryTerm != null) 'query': queryTerm,
|
||||||
|
if (mediaOnly != null) 'media': mediaOnly,
|
||||||
};
|
};
|
||||||
|
|
||||||
final response = await client.get(
|
final response = await client.get(
|
||||||
@@ -82,6 +94,14 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
final List<String>? tags;
|
final List<String>? tags;
|
||||||
final bool shuffle;
|
final bool shuffle;
|
||||||
final bool? pinned;
|
final bool? pinned;
|
||||||
|
final bool? includeReplies;
|
||||||
|
final bool? mediaOnly;
|
||||||
|
final String? queryTerm;
|
||||||
|
// Can be "populaurity", other value will be treated as "date"
|
||||||
|
final String? order;
|
||||||
|
final int? periodStart;
|
||||||
|
final int? periodEnd;
|
||||||
|
final bool? orderDesc;
|
||||||
final PostItemType itemType;
|
final PostItemType itemType;
|
||||||
final Color? backgroundColor;
|
final Color? backgroundColor;
|
||||||
final EdgeInsets? padding;
|
final EdgeInsets? padding;
|
||||||
@@ -99,6 +119,13 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
this.tags,
|
this.tags,
|
||||||
this.shuffle = false,
|
this.shuffle = false,
|
||||||
this.pinned,
|
this.pinned,
|
||||||
|
this.includeReplies,
|
||||||
|
this.mediaOnly,
|
||||||
|
this.queryTerm,
|
||||||
|
this.order,
|
||||||
|
this.orderDesc = true,
|
||||||
|
this.periodStart,
|
||||||
|
this.periodEnd,
|
||||||
this.itemType = PostItemType.regular,
|
this.itemType = PostItemType.regular,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
this.padding,
|
this.padding,
|
||||||
@@ -118,6 +145,13 @@ class SliverPostList extends HookConsumerWidget {
|
|||||||
tags: tags,
|
tags: tags,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc ?? true,
|
||||||
);
|
);
|
||||||
return PagingHelperSliverView(
|
return PagingHelperSliverView(
|
||||||
provider: provider,
|
provider: provider,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ part of 'post_list.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$postListNotifierHash() => r'fc139ad4df0deb67bcbb949560319f2f7fbfb503';
|
String _$postListNotifierHash() => r'8241120dc3c2004387c6cf881e5cb9224cbd3a97';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -39,6 +39,12 @@ abstract class _$PostListNotifier
|
|||||||
late final bool? pinned;
|
late final bool? pinned;
|
||||||
late final bool shuffle;
|
late final bool shuffle;
|
||||||
late final bool? includeReplies;
|
late final bool? includeReplies;
|
||||||
|
late final bool? mediaOnly;
|
||||||
|
late final String? queryTerm;
|
||||||
|
late final String? order;
|
||||||
|
late final int? periodStart;
|
||||||
|
late final int? periodEnd;
|
||||||
|
late final bool orderDesc;
|
||||||
|
|
||||||
FutureOr<CursorPagingData<SnPost>> build({
|
FutureOr<CursorPagingData<SnPost>> build({
|
||||||
String? pubName,
|
String? pubName,
|
||||||
@@ -49,6 +55,12 @@ abstract class _$PostListNotifier
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +84,12 @@ class PostListNotifierFamily
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) {
|
}) {
|
||||||
return PostListNotifierProvider(
|
return PostListNotifierProvider(
|
||||||
pubName: pubName,
|
pubName: pubName,
|
||||||
@@ -82,6 +100,12 @@ class PostListNotifierFamily
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +122,12 @@ class PostListNotifierFamily
|
|||||||
pinned: provider.pinned,
|
pinned: provider.pinned,
|
||||||
shuffle: provider.shuffle,
|
shuffle: provider.shuffle,
|
||||||
includeReplies: provider.includeReplies,
|
includeReplies: provider.includeReplies,
|
||||||
|
mediaOnly: provider.mediaOnly,
|
||||||
|
queryTerm: provider.queryTerm,
|
||||||
|
order: provider.order,
|
||||||
|
periodStart: provider.periodStart,
|
||||||
|
periodEnd: provider.periodEnd,
|
||||||
|
orderDesc: provider.orderDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +163,12 @@ class PostListNotifierProvider
|
|||||||
bool? pinned,
|
bool? pinned,
|
||||||
bool shuffle = false,
|
bool shuffle = false,
|
||||||
bool? includeReplies,
|
bool? includeReplies,
|
||||||
|
bool? mediaOnly,
|
||||||
|
String? queryTerm,
|
||||||
|
String? order,
|
||||||
|
int? periodStart,
|
||||||
|
int? periodEnd,
|
||||||
|
bool orderDesc = true,
|
||||||
}) : this._internal(
|
}) : this._internal(
|
||||||
() =>
|
() =>
|
||||||
PostListNotifier()
|
PostListNotifier()
|
||||||
@@ -143,7 +179,13 @@ class PostListNotifierProvider
|
|||||||
..tags = tags
|
..tags = tags
|
||||||
..pinned = pinned
|
..pinned = pinned
|
||||||
..shuffle = shuffle
|
..shuffle = shuffle
|
||||||
..includeReplies = includeReplies,
|
..includeReplies = includeReplies
|
||||||
|
..mediaOnly = mediaOnly
|
||||||
|
..queryTerm = queryTerm
|
||||||
|
..order = order
|
||||||
|
..periodStart = periodStart
|
||||||
|
..periodEnd = periodEnd
|
||||||
|
..orderDesc = orderDesc,
|
||||||
from: postListNotifierProvider,
|
from: postListNotifierProvider,
|
||||||
name: r'postListNotifierProvider',
|
name: r'postListNotifierProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
@@ -161,6 +203,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
|
|
||||||
PostListNotifierProvider._internal(
|
PostListNotifierProvider._internal(
|
||||||
@@ -178,6 +226,12 @@ class PostListNotifierProvider
|
|||||||
required this.pinned,
|
required this.pinned,
|
||||||
required this.shuffle,
|
required this.shuffle,
|
||||||
required this.includeReplies,
|
required this.includeReplies,
|
||||||
|
required this.mediaOnly,
|
||||||
|
required this.queryTerm,
|
||||||
|
required this.order,
|
||||||
|
required this.periodStart,
|
||||||
|
required this.periodEnd,
|
||||||
|
required this.orderDesc,
|
||||||
}) : super.internal();
|
}) : super.internal();
|
||||||
|
|
||||||
final String? pubName;
|
final String? pubName;
|
||||||
@@ -188,6 +242,12 @@ class PostListNotifierProvider
|
|||||||
final bool? pinned;
|
final bool? pinned;
|
||||||
final bool shuffle;
|
final bool shuffle;
|
||||||
final bool? includeReplies;
|
final bool? includeReplies;
|
||||||
|
final bool? mediaOnly;
|
||||||
|
final String? queryTerm;
|
||||||
|
final String? order;
|
||||||
|
final int? periodStart;
|
||||||
|
final int? periodEnd;
|
||||||
|
final bool orderDesc;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
FutureOr<CursorPagingData<SnPost>> runNotifierBuild(
|
||||||
@@ -202,6 +262,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +285,13 @@ class PostListNotifierProvider
|
|||||||
..tags = tags
|
..tags = tags
|
||||||
..pinned = pinned
|
..pinned = pinned
|
||||||
..shuffle = shuffle
|
..shuffle = shuffle
|
||||||
..includeReplies = includeReplies,
|
..includeReplies = includeReplies
|
||||||
|
..mediaOnly = mediaOnly
|
||||||
|
..queryTerm = queryTerm
|
||||||
|
..order = order
|
||||||
|
..periodStart = periodStart
|
||||||
|
..periodEnd = periodEnd
|
||||||
|
..orderDesc = orderDesc,
|
||||||
from: from,
|
from: from,
|
||||||
name: null,
|
name: null,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
@@ -233,6 +305,12 @@ class PostListNotifierProvider
|
|||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
shuffle: shuffle,
|
shuffle: shuffle,
|
||||||
includeReplies: includeReplies,
|
includeReplies: includeReplies,
|
||||||
|
mediaOnly: mediaOnly,
|
||||||
|
queryTerm: queryTerm,
|
||||||
|
order: order,
|
||||||
|
periodStart: periodStart,
|
||||||
|
periodEnd: periodEnd,
|
||||||
|
orderDesc: orderDesc,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -256,7 +334,13 @@ class PostListNotifierProvider
|
|||||||
other.tags == tags &&
|
other.tags == tags &&
|
||||||
other.pinned == pinned &&
|
other.pinned == pinned &&
|
||||||
other.shuffle == shuffle &&
|
other.shuffle == shuffle &&
|
||||||
other.includeReplies == includeReplies;
|
other.includeReplies == includeReplies &&
|
||||||
|
other.mediaOnly == mediaOnly &&
|
||||||
|
other.queryTerm == queryTerm &&
|
||||||
|
other.order == order &&
|
||||||
|
other.periodStart == periodStart &&
|
||||||
|
other.periodEnd == periodEnd &&
|
||||||
|
other.orderDesc == orderDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -270,6 +354,12 @@ class PostListNotifierProvider
|
|||||||
hash = _SystemHash.combine(hash, pinned.hashCode);
|
hash = _SystemHash.combine(hash, pinned.hashCode);
|
||||||
hash = _SystemHash.combine(hash, shuffle.hashCode);
|
hash = _SystemHash.combine(hash, shuffle.hashCode);
|
||||||
hash = _SystemHash.combine(hash, includeReplies.hashCode);
|
hash = _SystemHash.combine(hash, includeReplies.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, mediaOnly.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, queryTerm.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, order.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, periodStart.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, periodEnd.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, orderDesc.hashCode);
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
return _SystemHash.finish(hash);
|
||||||
}
|
}
|
||||||
@@ -302,6 +392,24 @@ mixin PostListNotifierRef
|
|||||||
|
|
||||||
/// The parameter `includeReplies` of this provider.
|
/// The parameter `includeReplies` of this provider.
|
||||||
bool? get includeReplies;
|
bool? get includeReplies;
|
||||||
|
|
||||||
|
/// The parameter `mediaOnly` of this provider.
|
||||||
|
bool? get mediaOnly;
|
||||||
|
|
||||||
|
/// The parameter `queryTerm` of this provider.
|
||||||
|
String? get queryTerm;
|
||||||
|
|
||||||
|
/// The parameter `order` of this provider.
|
||||||
|
String? get order;
|
||||||
|
|
||||||
|
/// The parameter `periodStart` of this provider.
|
||||||
|
int? get periodStart;
|
||||||
|
|
||||||
|
/// The parameter `periodEnd` of this provider.
|
||||||
|
int? get periodEnd;
|
||||||
|
|
||||||
|
/// The parameter `orderDesc` of this provider.
|
||||||
|
bool get orderDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PostListNotifierProviderElement
|
class _PostListNotifierProviderElement
|
||||||
@@ -331,6 +439,18 @@ class _PostListNotifierProviderElement
|
|||||||
@override
|
@override
|
||||||
bool? get includeReplies =>
|
bool? get includeReplies =>
|
||||||
(origin as PostListNotifierProvider).includeReplies;
|
(origin as PostListNotifierProvider).includeReplies;
|
||||||
|
@override
|
||||||
|
bool? get mediaOnly => (origin as PostListNotifierProvider).mediaOnly;
|
||||||
|
@override
|
||||||
|
String? get queryTerm => (origin as PostListNotifierProvider).queryTerm;
|
||||||
|
@override
|
||||||
|
String? get order => (origin as PostListNotifierProvider).order;
|
||||||
|
@override
|
||||||
|
int? get periodStart => (origin as PostListNotifierProvider).periodStart;
|
||||||
|
@override
|
||||||
|
int? get periodEnd => (origin as PostListNotifierProvider).periodEnd;
|
||||||
|
@override
|
||||||
|
bool get orderDesc => (origin as PostListNotifierProvider).orderDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
|
|||||||
@@ -542,6 +542,7 @@ class ReactionDetailsPopup extends HookConsumerWidget {
|
|||||||
notifierRefreshable: provider.notifier,
|
notifierRefreshable: provider.notifier,
|
||||||
contentBuilder:
|
contentBuilder:
|
||||||
(data, widgetCount, endItemView) => ListView.builder(
|
(data, widgetCount, endItemView) => ListView.builder(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
itemCount: widgetCount,
|
itemCount: widgetCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == widgetCount - 1) {
|
if (index == widgetCount - 1) {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
#include <flutter_timezone/flutter_timezone_plugin.h>
|
#include <flutter_timezone/flutter_timezone_plugin.h>
|
||||||
#include <flutter_udid/flutter_udid_plugin.h>
|
#include <flutter_udid/flutter_udid_plugin.h>
|
||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
|
||||||
#include <gtk/gtk_plugin.h>
|
|
||||||
#include <irondash_engine_context/irondash_engine_context_plugin.h>
|
#include <irondash_engine_context/irondash_engine_context_plugin.h>
|
||||||
#include <livekit_client/live_kit_plugin.h>
|
#include <livekit_client/live_kit_plugin.h>
|
||||||
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
|
||||||
@@ -51,9 +50,6 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin");
|
||||||
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
|
||||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
|
||||||
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
|
g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin");
|
||||||
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
|
irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
flutter_timezone
|
flutter_timezone
|
||||||
flutter_udid
|
flutter_udid
|
||||||
flutter_webrtc
|
flutter_webrtc
|
||||||
gtk
|
|
||||||
irondash_engine_context
|
irondash_engine_context
|
||||||
livekit_client
|
livekit_client
|
||||||
media_kit_libs_linux
|
media_kit_libs_linux
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
import app_links
|
|
||||||
import connectivity_plus
|
import connectivity_plus
|
||||||
import device_info_plus
|
import device_info_plus
|
||||||
import file_picker
|
import file_picker
|
||||||
@@ -31,6 +30,7 @@ import media_kit_video
|
|||||||
import package_info_plus
|
import package_info_plus
|
||||||
import pasteboard
|
import pasteboard
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import protocol_handler_macos
|
||||||
import record_macos
|
import record_macos
|
||||||
import screen_retriever_macos
|
import screen_retriever_macos
|
||||||
import share_plus
|
import share_plus
|
||||||
@@ -47,7 +47,6 @@ import wakelock_plus
|
|||||||
import window_manager
|
import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
|
|
||||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
@@ -73,6 +72,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
|
||||||
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
RecordMacOsPlugin.register(with: registry.registrar(forPlugin: "RecordMacOsPlugin"))
|
||||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- app_links (6.4.1):
|
|
||||||
- FlutterMacOS
|
|
||||||
- connectivity_plus (0.0.1):
|
- connectivity_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- croppy (0.0.1):
|
- croppy (0.0.1):
|
||||||
@@ -199,6 +197,8 @@ PODS:
|
|||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- PromisesSwift (2.4.0):
|
- PromisesSwift (2.4.0):
|
||||||
- PromisesObjC (= 2.4.0)
|
- PromisesObjC (= 2.4.0)
|
||||||
|
- protocol_handler_macos (0.0.1):
|
||||||
|
- FlutterMacOS
|
||||||
- record_macos (1.1.0):
|
- record_macos (1.1.0):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- SAMKeychain (1.5.3)
|
- SAMKeychain (1.5.3)
|
||||||
@@ -256,7 +256,6 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
|
|
||||||
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
- connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`)
|
||||||
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
- croppy (from `Flutter/ephemeral/.symlinks/plugins/croppy/macos`)
|
||||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||||
@@ -284,6 +283,7 @@ DEPENDENCIES:
|
|||||||
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
- pasteboard (from `Flutter/ephemeral/.symlinks/plugins/pasteboard/macos`)
|
||||||
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
|
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
|
||||||
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
- record_macos (from `Flutter/ephemeral/.symlinks/plugins/record_macos/macos`)
|
||||||
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
|
||||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
@@ -323,8 +323,6 @@ SPEC REPOS:
|
|||||||
- WebRTC-SDK
|
- WebRTC-SDK
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
app_links:
|
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/app_links/macos
|
|
||||||
connectivity_plus:
|
connectivity_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos
|
||||||
croppy:
|
croppy:
|
||||||
@@ -379,6 +377,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/pasteboard/macos
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
|
||||||
|
protocol_handler_macos:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
|
||||||
record_macos:
|
record_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/record_macos/macos
|
||||||
screen_retriever_macos:
|
screen_retriever_macos:
|
||||||
@@ -409,7 +409,6 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
|
|
||||||
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e
|
||||||
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
|
croppy: d9bfc8c02f3cd1851f669a421df298a474b78f43
|
||||||
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
|
||||||
@@ -454,6 +453,7 @@ SPEC CHECKSUMS:
|
|||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
|
||||||
|
protocol_handler_macos: f9cd7b13bcaf6b0425f7410cbe52376cb843a936
|
||||||
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
record_macos: 43194b6c06ca6f8fa132e2acea72b202b92a0f5b
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
|
||||||
|
|||||||
@@ -1,45 +1,58 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
|
||||||
<false/>
|
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
|
||||||
<key>CFBundleExecutable</key>
|
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string></string>
|
|
||||||
<key>CFBundleIdentifier</key>
|
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
|
||||||
<string>6.0</string>
|
|
||||||
<key>CFBundleName</key>
|
|
||||||
<string>Solian</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
|
||||||
<key>CFBundleVersion</key>
|
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
|
||||||
<key>NSHumanReadableCopyright</key>
|
|
||||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
|
||||||
<key>NSMainNibFile</key>
|
|
||||||
<string>MainMenu</string>
|
|
||||||
<key>LSApplicationCategoryType</key>
|
|
||||||
<string>public.app-category.social-networking</string>
|
|
||||||
<key>NSPrincipalClass</key>
|
|
||||||
<string>NSApplication</string>
|
|
||||||
<key>NSSupportsAutomaticTermination</key>
|
|
||||||
<false/>
|
|
||||||
<key>UIApplicationSceneManifest</key>
|
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<true/>
|
<false />
|
||||||
<key>UISceneConfigurations</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<dict/>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string></string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Solian</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
|
<key>NSHumanReadableCopyright</key>
|
||||||
|
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||||
|
<key>NSMainNibFile</key>
|
||||||
|
<string>MainMenu</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.social-networking</string>
|
||||||
|
<key>NSPrincipalClass</key>
|
||||||
|
<string>NSApplication</string>
|
||||||
|
<key>NSSupportsAutomaticTermination</key>
|
||||||
|
<false />
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<true />
|
||||||
|
<key>UISceneConfigurations</key>
|
||||||
|
<dict />
|
||||||
|
</dict>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string></string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>solian</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
|
||||||
</plist>
|
</plist>
|
||||||
204
pubspec.lock
204
pubspec.lock
@@ -13,10 +13,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: _flutterfire_internals
|
name: _flutterfire_internals
|
||||||
sha256: f871a7d1b686bea1f13722aa51ab31554d05c81f47054d6de48cc8c45153508b
|
sha256: "8a1f5f3020ef2a74fb93f7ab3ef127a8feea33a7a2276279113660784ee7516a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.63"
|
version: "1.3.64"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -49,38 +49,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.0.3"
|
||||||
app_links:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: app_links
|
|
||||||
sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "6.4.1"
|
|
||||||
app_links_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_links_linux
|
|
||||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.3"
|
|
||||||
app_links_platform_interface:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_links_platform_interface
|
|
||||||
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.2"
|
|
||||||
app_links_web:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_links_web
|
|
||||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.4"
|
|
||||||
archive:
|
archive:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -341,10 +309,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.6"
|
version: "3.0.7"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -429,10 +397,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: device_info_plus
|
name: device_info_plus
|
||||||
sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a"
|
sha256: "72d146c6d7098689ff5c5f66bcf593ac11efc530095385356e131070333e64da"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "11.5.0"
|
version: "11.3.0"
|
||||||
device_info_plus_platform_interface:
|
device_info_plus_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -637,34 +605,34 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_analytics
|
name: firebase_analytics
|
||||||
sha256: "3cfc4089e61e810ffb531af63cfde2c8cfd36f12dc14fdba359e623992311015"
|
sha256: bfb80d92eee10a6585ebd5a7e60de5caf0f2c06329e5676c0578130aea1bfe85
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "12.0.3"
|
version: "12.0.4"
|
||||||
firebase_analytics_platform_interface:
|
firebase_analytics_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_analytics_platform_interface
|
name: firebase_analytics_platform_interface
|
||||||
sha256: "775fc18d9b00a014362510a33f76f1f34deb30f69a64edcb41a7dfd0ebd9cf98"
|
sha256: "3b803077907def997044774f6c022d8e9204e9c0f5e205e3572d887c93dafd72"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.3"
|
version: "5.0.4"
|
||||||
firebase_analytics_web:
|
firebase_analytics_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_analytics_web
|
name: firebase_analytics_web
|
||||||
sha256: "6eafa8fef5fdca6c922ac3e353c9a093c12344a3ba996e65fd40f8db0a00d26f"
|
sha256: "0dbd96dbe77b51185319000c0078477fdcffb4abb0018c362dd9afb9845c1e06"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.0+3"
|
version: "0.6.1"
|
||||||
firebase_core:
|
firebase_core:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_core
|
name: firebase_core
|
||||||
sha256: "132e1c311bc41e7d387b575df0aacdf24efbf4930365eb61042be5bde3978f03"
|
sha256: "1f2dfd9f535d81f8b06d7a50ecda6eac1e6922191ed42e09ca2c84bd2288927c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.2.1"
|
||||||
firebase_core_platform_interface:
|
firebase_core_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -677,50 +645,50 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_core_web
|
name: firebase_core_web
|
||||||
sha256: ecde2def458292404a4fcd3731ee4992fd631a0ec359d2d67c33baa8da5ec8ae
|
sha256: ff18fabb0ad0ed3595d2f2c85007ecc794aadecdff5b3bb1460b7ee47cded398
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.0"
|
version: "3.3.0"
|
||||||
firebase_crashlytics:
|
firebase_crashlytics:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_crashlytics
|
name: firebase_crashlytics
|
||||||
sha256: "2f53d0d3c0875105b166f09bdf026026bb74f26930c6ffcd5d65b311ca5a9f58"
|
sha256: c3ebe3ed9f3b1d36c0864a4a28b041fcc2686f11fb2a4f7891319ea8d1d161cc
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.0.3"
|
version: "5.0.4"
|
||||||
firebase_crashlytics_platform_interface:
|
firebase_crashlytics_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_crashlytics_platform_interface
|
name: firebase_crashlytics_platform_interface
|
||||||
sha256: de5c857525fc9576cd3fc30fc72422bc2371179ecae110246c0135ae896c6de3
|
sha256: a8ca502fe3aa48b4f0b9e6e3bc0019085a247b5d1214cd342a189457975662db
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.8.14"
|
version: "3.8.15"
|
||||||
firebase_messaging:
|
firebase_messaging:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging
|
name: firebase_messaging
|
||||||
sha256: "5021279acd1cb5ccaceaa388e616e82cc4a2e4d862f02637df0e8ab766e6900a"
|
sha256: "22086f857d2340f5d973776cfd542d3fb30cf98e1c643c3aa4a7520bb12745bb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "16.0.3"
|
version: "16.0.4"
|
||||||
firebase_messaging_platform_interface:
|
firebase_messaging_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging_platform_interface
|
name: firebase_messaging_platform_interface
|
||||||
sha256: f3a16c51f02055ace2a7c16ccb341c1f1b36b67c13270a48bcef68c1d970bbe8
|
sha256: a59920cbf2eb7c83d34a5f354331210ffec116b216dc72d864d8b8eb983ca398
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.7.3"
|
version: "4.7.4"
|
||||||
firebase_messaging_web:
|
firebase_messaging_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: firebase_messaging_web
|
name: firebase_messaging_web
|
||||||
sha256: "3eb9a1382caeb95b370f21e36d4a460496af777c9c2ef5df9b90d4803982c069"
|
sha256: "1183e40e6fd2a279a628951cc3b639fcf5ffe7589902632db645011eb70ebefb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.3"
|
version: "4.1.0"
|
||||||
fixnum:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -778,10 +746,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_card_swiper
|
name: flutter_card_swiper
|
||||||
sha256: "9fbe75c913c0a01f34f9f98068ad198e396695fcf8abfa433cc53652fceb5617"
|
sha256: "895c6974729b51cf73a35f1b58ab57a0af3293131319e2cbccac3bc57ffcd69f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.1.0"
|
version: "7.2.0"
|
||||||
flutter_colorpicker:
|
flutter_colorpicker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -1103,10 +1071,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: flutter_svg
|
name: flutter_svg
|
||||||
sha256: b9c2ad5872518a27507ab432d1fb97e8813b05f0fc693f9d40fad06d073e0678
|
sha256: "055de8921be7b8e8b98a233c7a5ef84b3a6fcc32f46f1ebf5b9bb3576d108355"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.2"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -1201,10 +1169,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: get_it
|
name: get_it
|
||||||
sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b
|
sha256: ae78de7c3f2304b8d81f2bb6e320833e5e81de942188542328f074978cc0efa9
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.2.0"
|
version: "8.3.0"
|
||||||
glob:
|
glob:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1245,14 +1213,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.3.4"
|
version: "5.3.4"
|
||||||
gtk:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: gtk
|
|
||||||
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
highlight:
|
highlight:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1957,6 +1917,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.0"
|
version: "4.2.0"
|
||||||
|
protocol_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: protocol_handler
|
||||||
|
sha256: dc2e2dcb1e0e313c3f43827ec3fa6d98adee6e17edc0c3923ac67efee87479a9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
protocol_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protocol_handler_android
|
||||||
|
sha256: "82eb860ca42149e400328f54b85140329a1766d982e94705b68271f6ca73895c"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
protocol_handler_ios:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protocol_handler_ios
|
||||||
|
sha256: "0d3a56b8c1926002cb1e32b46b56874759f4dcc8183d389b670864ac041b6ec2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
protocol_handler_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protocol_handler_macos
|
||||||
|
sha256: "6eb8687a84e7da3afbc5660ce046f29d7ecf7976db45a9dadeae6c87147dd710"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
protocol_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protocol_handler_platform_interface
|
||||||
|
sha256: "53776b10526fdc25efdf1abcf68baf57fdfdb75342f4101051db521c9e3f3e5b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
|
protocol_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: protocol_handler_windows
|
||||||
|
sha256: d8f3a58938386aca2c76292757392f4d059d09f11439d6d896d876ebe997f2c4
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.0"
|
||||||
provider:
|
provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2395,14 +2403,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.1"
|
||||||
sprintf:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sprintf
|
|
||||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "7.0.0"
|
|
||||||
sqflite:
|
sqflite:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2551,18 +2551,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_core
|
name: syncfusion_flutter_core
|
||||||
sha256: a24e9ec04e03c2c14b7b41b1afe60e455adef09b244ab4c425ce6c5b8f58c9ce
|
sha256: "825670efc828e18e14ff310cbcf6de91c8ff73b55c75ae6868a2b3cd87e88b6c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.4"
|
version: "31.2.5"
|
||||||
syncfusion_flutter_pdf:
|
syncfusion_flutter_pdf:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_pdf
|
name: syncfusion_flutter_pdf
|
||||||
sha256: "8d98edae5c5d3aba2125de49bd37882da124409021d4f3de5730eb93d8247a81"
|
sha256: "50fc39ba628167949e89374488de67cc788646d6c0dce2a9fd047dbeecb841c2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.4"
|
version: "31.2.5"
|
||||||
syncfusion_flutter_pdfviewer:
|
syncfusion_flutter_pdfviewer:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -2575,50 +2575,50 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_flutter_signaturepad
|
name: syncfusion_flutter_signaturepad
|
||||||
sha256: d2f87273133283efd550370403462739329ad0ad1bdae6a73998be1fb30e9ee1
|
sha256: "6e60af61cec5ee7436b01ecb3fd944602aed42887789a67a27314678ad04d38a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.4"
|
version: "31.2.5"
|
||||||
syncfusion_pdfviewer_linux:
|
syncfusion_pdfviewer_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_linux
|
name: syncfusion_pdfviewer_linux
|
||||||
sha256: "1edc9c3408526ad25c7a0d67b0f12a3e427225fd7e87d67319cd6e19bbfaeb45"
|
sha256: fea25c996ed8850504c80c8fe7541aa3dce3d5159af0e92519d13e10a9509601
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.4"
|
version: "31.2.5"
|
||||||
syncfusion_pdfviewer_macos:
|
syncfusion_pdfviewer_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_macos
|
name: syncfusion_pdfviewer_macos
|
||||||
sha256: "962911d8cba4d3f5f0bf5dee5ef87cc0b31651431adfad56a51c47057859fb50"
|
sha256: "389326ef84ad9d14858d4f5f14da36267faa894134c38080ae30d55d2e3f4ce9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.4"
|
version: "31.2.5"
|
||||||
syncfusion_pdfviewer_platform_interface:
|
syncfusion_pdfviewer_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_platform_interface
|
name: syncfusion_pdfviewer_platform_interface
|
||||||
sha256: a701825a971f1bb8540ad39611872ebc08ed0955a0a9600f263cb6cb85826ce2
|
sha256: "2c3098dc644965feee66f4bf726ef433a51eecc16ccea71e052ba19897f3c2c5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.4"
|
version: "31.2.5"
|
||||||
syncfusion_pdfviewer_web:
|
syncfusion_pdfviewer_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_web
|
name: syncfusion_pdfviewer_web
|
||||||
sha256: e3eda11636a013a7ebab01a573b079d3a52c695474ac7c5239f65d5952d8da82
|
sha256: db5b91493aefb2e9faeb6425ea4f3c5f8eb7907a29ffca2e33564987a9c1c1f4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.4"
|
version: "31.2.5"
|
||||||
syncfusion_pdfviewer_windows:
|
syncfusion_pdfviewer_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: syncfusion_pdfviewer_windows
|
name: syncfusion_pdfviewer_windows
|
||||||
sha256: "9f8def51da7277bda5796ba27fff357a697689e226be397d7c52e353824cf961"
|
sha256: d2e4d64e5cd96ea678b1ff66588897ce59e17e2685c1153995af53d91327a143
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "31.2.4"
|
version: "31.2.5"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2719,10 +2719,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: tray_manager
|
name: tray_manager
|
||||||
sha256: "537e539f48cd82d8ee2240d4330158c7b44c7e043e8e18b5811f2f8f6b7df25a"
|
sha256: c5fd83b0ae4d80be6eaedfad87aaefab8787b333b8ebd064b0e442a81006035b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.1"
|
version: "0.5.2"
|
||||||
tuple:
|
tuple:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2840,10 +2840,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.5.1"
|
version: "4.5.2"
|
||||||
vector_graphics:
|
vector_graphics:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2984,10 +2984,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: win32_registry
|
name: win32_registry
|
||||||
sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae"
|
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
version: "1.1.5"
|
||||||
window_manager:
|
window_manager:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
24
pubspec.yaml
24
pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# 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
|
# 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.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 3.3.0+143
|
version: 3.3.0+145
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.7.2
|
sdk: ^3.7.2
|
||||||
@@ -50,7 +50,7 @@ dependencies:
|
|||||||
flutter_markdown_latex: ^0.3.4
|
flutter_markdown_latex: ^0.3.4
|
||||||
markdown: ^7.3.0
|
markdown: ^7.3.0
|
||||||
flutter_highlight: ^0.7.0
|
flutter_highlight: ^0.7.0
|
||||||
uuid: ^4.5.1
|
uuid: ^4.5.2
|
||||||
url_launcher: ^6.3.2
|
url_launcher: ^6.3.2
|
||||||
google_fonts: ^6.3.2
|
google_fonts: ^6.3.2
|
||||||
gap: ^3.0.1
|
gap: ^3.0.1
|
||||||
@@ -67,7 +67,8 @@ dependencies:
|
|||||||
flutter_inappwebview: ^6.1.5
|
flutter_inappwebview: ^6.1.5
|
||||||
animations: ^2.1.0
|
animations: ^2.1.0
|
||||||
package_info_plus: ^9.0.0
|
package_info_plus: ^9.0.0
|
||||||
device_info_plus: ^11.5.0
|
device_info_plus: ^11.3.0
|
||||||
|
protocol_handler: ^0.2.0
|
||||||
tus_client_dart:
|
tus_client_dart:
|
||||||
git: https://github.com/LittleSheep2Code/tus_client.git
|
git: https://github.com/LittleSheep2Code/tus_client.git
|
||||||
cross_file: ^0.3.5
|
cross_file: ^0.3.5
|
||||||
@@ -78,9 +79,9 @@ dependencies:
|
|||||||
image_picker_android: ^0.8.13+7
|
image_picker_android: ^0.8.13+7
|
||||||
super_context_menu: ^0.9.1
|
super_context_menu: ^0.9.1
|
||||||
modal_bottom_sheet: ^3.0.0
|
modal_bottom_sheet: ^3.0.0
|
||||||
firebase_messaging: ^16.0.3
|
firebase_messaging: ^16.0.4
|
||||||
flutter_udid: ^4.0.0
|
flutter_udid: ^4.0.0
|
||||||
firebase_core: ^4.2.0
|
firebase_core: ^4.2.1
|
||||||
web_socket_channel: ^3.0.3
|
web_socket_channel: ^3.0.3
|
||||||
material_symbols_icons: ^4.2874.0
|
material_symbols_icons: ^4.2874.0
|
||||||
drift: ^2.28.2
|
drift: ^2.28.2
|
||||||
@@ -93,7 +94,7 @@ dependencies:
|
|||||||
relative_time: ^5.0.0
|
relative_time: ^5.0.0
|
||||||
dropdown_button2: ^2.3.9
|
dropdown_button2: ^2.3.9
|
||||||
riverpod_paging_utils: ^0.8.1
|
riverpod_paging_utils: ^0.8.1
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.7
|
||||||
avatar_stack: ^3.0.0
|
avatar_stack: ^3.0.0
|
||||||
markdown_widget: ^2.3.2+8
|
markdown_widget: ^2.3.2+8
|
||||||
visibility_detector: ^0.4.0+2
|
visibility_detector: ^0.4.0+2
|
||||||
@@ -115,7 +116,7 @@ dependencies:
|
|||||||
flutter_timezone: ^5.0.1
|
flutter_timezone: ^5.0.1
|
||||||
fl_chart: ^1.1.1
|
fl_chart: ^1.1.1
|
||||||
sign_in_with_apple: ^7.0.1
|
sign_in_with_apple: ^7.0.1
|
||||||
flutter_svg: ^2.2.1
|
flutter_svg: ^2.2.2
|
||||||
native_exif: ^0.6.2
|
native_exif: ^0.6.2
|
||||||
local_auth: ^3.0.0
|
local_auth: ^3.0.0
|
||||||
flutter_secure_storage: ^9.2.4
|
flutter_secure_storage: ^9.2.4
|
||||||
@@ -134,13 +135,13 @@ dependencies:
|
|||||||
flutter_app_update: ^3.2.2
|
flutter_app_update: ^3.2.2
|
||||||
archive: ^4.0.7
|
archive: ^4.0.7
|
||||||
process_run: ^1.2.4
|
process_run: ^1.2.4
|
||||||
firebase_crashlytics: ^5.0.3
|
firebase_crashlytics: ^5.0.4
|
||||||
firebase_analytics: ^12.0.3
|
firebase_analytics: ^12.0.4
|
||||||
material_color_utilities: ^0.11.1
|
material_color_utilities: ^0.11.1
|
||||||
screenshot: ^3.0.0
|
screenshot: ^3.0.0
|
||||||
flutter_card_swiper: ^7.1.0
|
flutter_card_swiper: ^7.2.0
|
||||||
file_saver: ^0.3.1
|
file_saver: ^0.3.1
|
||||||
tray_manager: ^0.5.1
|
tray_manager: ^0.5.2
|
||||||
flutter_webrtc: ^1.2.0
|
flutter_webrtc: ^1.2.0
|
||||||
flutter_local_notifications: ^19.5.0
|
flutter_local_notifications: ^19.5.0
|
||||||
wakelock_plus: ^1.4.0
|
wakelock_plus: ^1.4.0
|
||||||
@@ -158,7 +159,6 @@ dependencies:
|
|||||||
talker_logger: ^5.0.2
|
talker_logger: ^5.0.2
|
||||||
talker_dio_logger: ^5.0.2
|
talker_dio_logger: ^5.0.2
|
||||||
talker_riverpod_logger: ^5.0.1
|
talker_riverpod_logger: ^5.0.1
|
||||||
app_links: ^6.4.1
|
|
||||||
syncfusion_flutter_pdfviewer: ^31.1.21
|
syncfusion_flutter_pdfviewer: ^31.1.21
|
||||||
swipe_to: ^1.0.6
|
swipe_to: ^1.0.6
|
||||||
fl_heatmap: ^0.4.6
|
fl_heatmap: ^0.4.6
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
; ==================================================
|
; ==================================================
|
||||||
#define AppVersion "3.2.0"
|
#define AppVersion "3.3.0"
|
||||||
#define BuildNumber "134"
|
#define BuildNumber "144"
|
||||||
; ==================================================
|
; ==================================================
|
||||||
|
|
||||||
#define FullVersion AppVersion + "." + BuildNumber
|
#define FullVersion AppVersion + "." + BuildNumber
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <app_links/app_links_plugin_c_api.h>
|
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <dart_ipc/dart_ipc_plugin_c_api.h>
|
#include <dart_ipc/dart_ipc_plugin_c_api.h>
|
||||||
#include <file_saver/file_saver_plugin.h>
|
#include <file_saver/file_saver_plugin.h>
|
||||||
@@ -25,6 +24,7 @@
|
|||||||
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
#include <media_kit_libs_windows_video/media_kit_libs_windows_video_plugin_c_api.h>
|
||||||
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
#include <media_kit_video/media_kit_video_plugin_c_api.h>
|
||||||
#include <pasteboard/pasteboard_plugin.h>
|
#include <pasteboard/pasteboard_plugin.h>
|
||||||
|
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
|
||||||
#include <record_windows/record_windows_plugin_c_api.h>
|
#include <record_windows/record_windows_plugin_c_api.h>
|
||||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
@@ -38,8 +38,6 @@
|
|||||||
#include <windows_notification/windows_notification_plugin_c_api.h>
|
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
AppLinksPluginCApiRegisterWithRegistrar(
|
|
||||||
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
|
|
||||||
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
DartIpcPluginCApiRegisterWithRegistrar(
|
DartIpcPluginCApiRegisterWithRegistrar(
|
||||||
@@ -76,6 +74,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi"));
|
||||||
PasteboardPluginRegisterWithRegistrar(
|
PasteboardPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
registry->GetRegistrarForPlugin("PasteboardPlugin"));
|
||||||
|
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
|
||||||
RecordWindowsPluginCApiRegisterWithRegistrar(
|
RecordWindowsPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("RecordWindowsPluginCApi"));
|
||||||
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
app_links
|
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
dart_ipc
|
dart_ipc
|
||||||
file_saver
|
file_saver
|
||||||
@@ -22,6 +21,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
media_kit_libs_windows_video
|
media_kit_libs_windows_video
|
||||||
media_kit_video
|
media_kit_video
|
||||||
pasteboard
|
pasteboard
|
||||||
|
protocol_handler_windows
|
||||||
record_windows
|
record_windows
|
||||||
screen_retriever_windows
|
screen_retriever_windows
|
||||||
share_plus
|
share_plus
|
||||||
|
|||||||
@@ -1,51 +1,23 @@
|
|||||||
#include <flutter/dart_project.h>
|
#include <flutter/dart_project.h>
|
||||||
#include <flutter/flutter_view_controller.h>
|
#include <flutter/flutter_view_controller.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include "app_links/app_links_plugin_c_api.h"
|
|
||||||
|
|
||||||
#include "flutter_window.h"
|
#include "flutter_window.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
bool SendAppLinkToInstance(const std::wstring& title) {
|
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
|
||||||
// Find our exact window
|
|
||||||
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str());
|
|
||||||
|
|
||||||
if (hwnd) {
|
|
||||||
// Dispatch new link to current window
|
|
||||||
SendAppLink(hwnd);
|
|
||||||
|
|
||||||
// (Optional) Restore our window to front in same state
|
|
||||||
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
|
|
||||||
GetWindowPlacement(hwnd, &place);
|
|
||||||
|
|
||||||
switch(place.showCmd) {
|
|
||||||
case SW_SHOWMAXIMIZED:
|
|
||||||
ShowWindow(hwnd, SW_SHOWMAXIMIZED);
|
|
||||||
break;
|
|
||||||
case SW_SHOWMINIMIZED:
|
|
||||||
ShowWindow(hwnd, SW_RESTORE);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ShowWindow(hwnd, SW_NORMAL);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
|
|
||||||
SetForegroundWindow(hwnd);
|
|
||||||
// END (Optional) Restore
|
|
||||||
|
|
||||||
// Window has been found, don't create another one.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
_In_ wchar_t *command_line, _In_ int show_command)
|
_In_ wchar_t *command_line, _In_ int show_command)
|
||||||
{
|
{
|
||||||
if (SendAppLinkToInstance(L"solian")) {
|
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"Solian");
|
||||||
return EXIT_SUCCESS;
|
if (hwnd != NULL)
|
||||||
|
{
|
||||||
|
DispatchToProtocolHandler(hwnd);
|
||||||
|
|
||||||
|
::ShowWindow(hwnd, SW_NORMAL);
|
||||||
|
::SetForegroundWindow(hwnd);
|
||||||
|
return EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach to console when present (e.g., 'flutter run') or create a
|
// Attach to console when present (e.g., 'flutter run') or create a
|
||||||
|
|||||||
Reference in New Issue
Block a user