✨ Post recommendation widget
This commit is contained in:
parent
e920bd954c
commit
8bdaf05223
@ -37,13 +37,6 @@ target 'Runner' do
|
||||
end
|
||||
end
|
||||
|
||||
target 'SolarWidget' do
|
||||
use_frameworks!
|
||||
use_modular_headers!
|
||||
|
||||
pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
|
@ -50,7 +50,7 @@
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
73DA8A022D05C7620024A03E /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
buildActionMask = 12;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
@ -113,20 +113,28 @@
|
||||
);
|
||||
target = 738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */;
|
||||
};
|
||||
738C1F502D0D91D000A215F3 /* Exceptions for "Data" folder in "Runner" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
User.swift,
|
||||
);
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
};
|
||||
738C1F512D0D91D000A215F3 /* Exceptions for "Data" folder in "SolarWidgetExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Post.swift,
|
||||
User.swift,
|
||||
);
|
||||
target = 738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */;
|
||||
};
|
||||
73BC73712D0DDF6300956BE0 /* Exceptions for "Service" folder in "SolarNotifyService" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Attachment.swift,
|
||||
);
|
||||
target = 73DA89F92D05C7620024A03E /* SolarNotifyService */;
|
||||
};
|
||||
73BC73722D0DDF6300956BE0 /* Exceptions for "Service" folder in "SolarWidgetExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Attachment.swift,
|
||||
);
|
||||
target = 738C1EAA2D0D76A400A215F3 /* SolarWidgetExtension */;
|
||||
};
|
||||
73DA8A062D05C7620024A03E /* Exceptions for "SolarNotifyService" folder in "SolarNotifyService" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
@ -148,12 +156,20 @@
|
||||
738C1F4F2D0D91CC00A215F3 /* Data */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
738C1F502D0D91D000A215F3 /* Exceptions for "Data" folder in "Runner" target */,
|
||||
738C1F512D0D91D000A215F3 /* Exceptions for "Data" folder in "SolarWidgetExtension" target */,
|
||||
);
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
73BC736C2D0DDF5600956BE0 /* Service */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
73BC73712D0DDF6300956BE0 /* Exceptions for "Service" folder in "SolarNotifyService" target */,
|
||||
73BC73722D0DDF6300956BE0 /* Exceptions for "Service" folder in "SolarWidgetExtension" target */,
|
||||
);
|
||||
path = Service;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
73DA89FB2D05C7620024A03E /* SolarNotifyService */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
@ -260,6 +276,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
73BC736C2D0DDF5600956BE0 /* Service */,
|
||||
738C1F4F2D0D91CC00A215F3 /* Data */,
|
||||
73111C212CEE3D5E004CF4B3 /* Runner.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
@ -378,6 +395,7 @@
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
738C1F4F2D0D91CC00A215F3 /* Data */,
|
||||
73BC736C2D0DDF5600956BE0 /* Service */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
|
38
ios/Runner/Data/Post.swift
Normal file
38
ios/Runner/Data/Post.swift
Normal file
@ -0,0 +1,38 @@
|
||||
//
|
||||
// SolarPost.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by LittleSheep on 2024/12/14.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
struct SolarPost : Codable {
|
||||
let id: Int
|
||||
let body: SolarPostBody
|
||||
let publisher: SolarPublisher
|
||||
let publisherId: Int
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
let editedAt: Date?
|
||||
let publishedAt: Date?
|
||||
}
|
||||
|
||||
struct SolarPostBody : Codable {
|
||||
let content: String?
|
||||
let title: String?
|
||||
let description: String?
|
||||
let attachments: [String]?
|
||||
}
|
||||
|
||||
struct SolarPublisher : Codable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let nick: String
|
||||
let description: String?
|
||||
let avatar: String?
|
||||
let banner: String?
|
||||
let createdAt: Date
|
||||
let updatedAt: Date
|
||||
}
|
14
ios/Runner/Service/Attachment.swift
Normal file
14
ios/Runner/Service/Attachment.swift
Normal file
@ -0,0 +1,14 @@
|
||||
//
|
||||
// Attachment.swift
|
||||
// Runner
|
||||
//
|
||||
// Created by LittleSheep on 2024/12/14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func getAttachmentUrl(for identifier: String) -> String {
|
||||
let serverBaseUrl = "https://api.sn.solsynth.dev"
|
||||
|
||||
return identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/cgi/uc/attachments/\(identifier)"
|
||||
}
|
@ -17,11 +17,6 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
private var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
private var bestAttemptContent: UNMutableNotificationContent?
|
||||
private let serverBaseUrl = "https://api.sn.solsynth.dev"
|
||||
|
||||
private func getAttachmentUrl(for identifier: String) -> String {
|
||||
identifier.starts(with: "http") ? identifier : "\(serverBaseUrl)/cgi/uc/attachments/\(identifier)"
|
||||
}
|
||||
|
||||
private func fetchAvatarImage(from url: String, completion: @escaping (INImage?) -> Void) {
|
||||
guard let imageURL = URL(string: url) else {
|
||||
|
@ -8,12 +8,12 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), user: nil, checkIn: nil)
|
||||
struct CheckInProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> CheckInEntry {
|
||||
CheckInEntry(date: Date(), user: nil, checkIn: nil)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
|
||||
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
@ -35,7 +35,7 @@ struct Provider: TimelineProvider {
|
||||
checkIn = try! jsonDecoder.decode(SolarCheckInRecord.self, from: checkInRaw.data(using: .utf8)!)
|
||||
}
|
||||
|
||||
let entry = SimpleEntry(
|
||||
let entry = CheckInEntry(
|
||||
date: Date(),
|
||||
user: user,
|
||||
checkIn: checkIn
|
||||
@ -51,14 +51,14 @@ struct Provider: TimelineProvider {
|
||||
}
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
struct CheckInEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let user: SolarUser?
|
||||
let checkIn: SolarCheckInRecord?
|
||||
}
|
||||
|
||||
struct SolarWidgetEntryView : View {
|
||||
var entry: Provider.Entry
|
||||
struct CheckInWidgetEntryView : View {
|
||||
var entry: CheckInProvider.Entry
|
||||
|
||||
private let resultTierSymbols: [String] = ["大凶", "凶", "中平", "大吉", "吉"]
|
||||
|
||||
@ -110,15 +110,15 @@ struct SolarWidgetEntryView : View {
|
||||
}
|
||||
|
||||
struct CheckInWidget: Widget {
|
||||
let kind: String = "SolarWidget"
|
||||
let kind: String = "SolarCheckInWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
StaticConfiguration(kind: kind, provider: CheckInProvider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
SolarWidgetEntryView(entry: entry)
|
||||
CheckInWidgetEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
SolarWidgetEntryView(entry: entry)
|
||||
CheckInWidgetEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
@ -132,8 +132,8 @@ struct CheckInWidget: Widget {
|
||||
#Preview(as: .systemSmall) {
|
||||
CheckInWidget()
|
||||
} timeline: {
|
||||
SimpleEntry(date: .now, user: nil, checkIn: nil)
|
||||
SimpleEntry(
|
||||
CheckInEntry(date: .now, user: nil, checkIn: nil)
|
||||
CheckInEntry(
|
||||
date: .now,
|
||||
user: SolarUser(id: 1, name: "demo", nick: "Deemo"),
|
||||
checkIn: SolarCheckInRecord(id: 1, resultTier: 1, resultExperience: 100, createdAt: Date.now)
|
||||
|
@ -5,3 +5,237 @@
|
||||
// Created by LittleSheep on 2024/12/14.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct FeaturedPostProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> FeaturedPostEntry {
|
||||
FeaturedPostEntry(date: Date(), user: nil, featuredPost: nil, family: .systemMedium)
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (FeaturedPostEntry) -> ()) {
|
||||
let prefs = UserDefaults(suiteName: "group.solsynth.solian")
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'"
|
||||
|
||||
let jsonDecoder = JSONDecoder()
|
||||
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let userRaw = prefs?.string(forKey: "user")
|
||||
var user: SolarUser?
|
||||
if let userRaw = userRaw {
|
||||
user = try! jsonDecoder.decode(SolarUser.self, from: userRaw.data(using: .utf8)!)
|
||||
}
|
||||
|
||||
let featuredPostRaw = prefs?.string(forKey: "post_featured")
|
||||
var featuredPosts: [SolarPost]?
|
||||
if let featuredPostRaw = featuredPostRaw {
|
||||
featuredPosts = try! jsonDecoder.decode([SolarPost].self, from: featuredPostRaw.data(using: .utf8)!)
|
||||
}
|
||||
|
||||
let entry = FeaturedPostEntry(
|
||||
date: Date(),
|
||||
user: user,
|
||||
featuredPost: featuredPosts?.first,
|
||||
family: context.family
|
||||
)
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
getSnapshot(in: context) { (entry) in
|
||||
let timeline = Timeline(entries: [entry], policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeaturedPostEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let user: SolarUser?
|
||||
let featuredPost: SolarPost?
|
||||
|
||||
let family: WidgetFamily
|
||||
}
|
||||
|
||||
struct FeaturedPostWidgetEntryView : View {
|
||||
var entry: FeaturedPostProvider.Entry
|
||||
|
||||
private let resultTierSymbols: [String] = ["大凶", "凶", "中平", "大吉", "吉"]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let featuredPost = entry.featuredPost {
|
||||
HStack(alignment: .center) {
|
||||
if let avatar = featuredPost.publisher.avatar {
|
||||
let avatarUrl = getAttachmentUrl(for: avatar)
|
||||
let size: CGFloat = 24
|
||||
|
||||
AsyncImage(url: URL(string: avatarUrl)) { image in
|
||||
image.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.cornerRadius(size / 2)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(Color.white, lineWidth: 4)
|
||||
.frame(width: size, height: size)
|
||||
)
|
||||
.shadow(radius: 10)
|
||||
.frame(width: 24, height: 24, alignment: .center)
|
||||
} placeholder: {
|
||||
ProgressView().frame(width: 24, height: 24, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
Text("@\(featuredPost.publisher.name)")
|
||||
.font(.system(size: 13, design: .monospaced))
|
||||
.opacity(0.9)
|
||||
|
||||
Spacer()
|
||||
}.frame(maxWidth: .infinity).padding(.bottom, 12)
|
||||
|
||||
if featuredPost.body.title != nil || featuredPost.body.description != nil {
|
||||
VStack(alignment: .leading) {
|
||||
if let title = featuredPost.body.title {
|
||||
Text(title)
|
||||
.font(.system(size: 17))
|
||||
}
|
||||
if let description = featuredPost.body.description {
|
||||
Text(description)
|
||||
.font(.system(size: 15))
|
||||
}
|
||||
}.padding(.bottom, 8)
|
||||
}
|
||||
|
||||
if let content = featuredPost.body.content {
|
||||
if (featuredPost.body.title == nil && featuredPost.body.description == nil) || entry.family == .systemLarge || entry.family == .systemExtraLarge {
|
||||
Text(
|
||||
(entry.family == .systemLarge || entry.family == .systemExtraLarge) ? content : content.replacingOccurrences(of: "\n", with: " ")
|
||||
)
|
||||
.font(.system(size: 15))
|
||||
} else {
|
||||
Text("\(Image(systemName: "plus")) total \(content.count) characters")
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.opacity(0.75)
|
||||
.padding(.top, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if let attachment = featuredPost.body.attachments {
|
||||
if attachment.count == 1 {
|
||||
Text("\(Image(systemName: "document.fill")) \(attachment.count) attachment")
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.opacity(0.75)
|
||||
.padding(.top, 1)
|
||||
} else if attachment.count > 1 {
|
||||
Text("\(Image(systemName: "document.fill")) \(attachment.count) attachments")
|
||||
.font(.system(size: 11, design: .monospaced))
|
||||
.opacity(0.75)
|
||||
.padding(.top, 1)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(featuredPost.publishedAt!, format: .dateTime)
|
||||
.font(.system(size: 11))
|
||||
Text("Solar Network Featured Posts")
|
||||
.font(.system(size: 9))
|
||||
} else {
|
||||
VStack(alignment: .center) {
|
||||
Text("No Recommendations").font(.system(size: 19, weight: .bold))
|
||||
Text("Click the widget to open the app to load featured posts")
|
||||
.font(.system(size: 15))
|
||||
.multilineTextAlignment(.center)
|
||||
}.frame(alignment: .center)
|
||||
}
|
||||
}.padding(8).frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
struct FeaturedPostWidget: Widget {
|
||||
let kind: String = "SolarFeaturedPostWidget"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: FeaturedPostProvider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
FeaturedPostWidgetEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
FeaturedPostWidgetEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("Featured Posts")
|
||||
.description("View the featured posts on the Solar Network")
|
||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge, .systemExtraLarge])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
FeaturedPostWidget()
|
||||
} timeline: {
|
||||
FeaturedPostEntry(date: Date.now, user: nil, featuredPost: nil, family: .systemLarge)
|
||||
FeaturedPostEntry(
|
||||
date: .now,
|
||||
user: SolarUser(id: 1, name: "demo", nick: "Deemo"),
|
||||
featuredPost: SolarPost(
|
||||
id: 1,
|
||||
body: SolarPostBody(
|
||||
content: "Hello, World",
|
||||
title: nil,
|
||||
description: nil,
|
||||
attachments: ["zb2hiUEmYcnpHfVN"]
|
||||
),
|
||||
publisher: SolarPublisher(
|
||||
id: 1,
|
||||
name: "demo",
|
||||
nick: "Deemo",
|
||||
description: nil,
|
||||
avatar: "IZxCFkJUPKRijFCx",
|
||||
banner: nil,
|
||||
createdAt: .now,
|
||||
updatedAt: .now
|
||||
),
|
||||
publisherId: 1,
|
||||
createdAt: .now,
|
||||
updatedAt: .now,
|
||||
editedAt: nil,
|
||||
publishedAt: .now
|
||||
),
|
||||
family: .systemSmall
|
||||
)
|
||||
FeaturedPostEntry(
|
||||
date: .now,
|
||||
user: SolarUser(id: 1, name: "demo", nick: "Deemo"),
|
||||
featuredPost: SolarPost(
|
||||
id: 1,
|
||||
body: SolarPostBody(
|
||||
content: "Hello, World\nOh wow",
|
||||
title: "Title",
|
||||
description: "Description",
|
||||
attachments: ["zb2hiUEmYcnpHfVN"]
|
||||
),
|
||||
publisher: SolarPublisher(
|
||||
id: 1,
|
||||
name: "demo",
|
||||
nick: "Deemo",
|
||||
description: nil,
|
||||
avatar: "IZxCFkJUPKRijFCx",
|
||||
banner: nil,
|
||||
createdAt: .now,
|
||||
updatedAt: .now
|
||||
),
|
||||
publisherId: 1,
|
||||
createdAt: .now,
|
||||
updatedAt: .now,
|
||||
editedAt: nil,
|
||||
publishedAt: .now
|
||||
),
|
||||
family: .systemLarge
|
||||
)
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import SwiftUI
|
||||
@main
|
||||
struct SolarWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
CheckInWidget()
|
||||
// CheckInWidget()
|
||||
FeaturedPostWidget()
|
||||
}
|
||||
}
|
||||
|
@ -15,18 +15,22 @@ class HomeWidgetProvider {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveWidgetData(String id, dynamic data) async {
|
||||
Future<void> saveWidgetData(String id, dynamic data, {bool update = true}) async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
|
||||
await HomeWidget.saveWidgetData(id, jsonEncode(data));
|
||||
if (update) await updateWidget();
|
||||
}
|
||||
|
||||
Future<void> updateWidget() async {
|
||||
if (kIsWeb || !(Platform.isAndroid || Platform.isIOS)) return;
|
||||
await HomeWidget.updateWidget(
|
||||
name: "SolarWidget",
|
||||
iOSName: "SolarWidget",
|
||||
androidName: "com.solsynth.solian.SolarWidget",
|
||||
qualifiedAndroidName: "group.solsynth.solian.SolarWidget",
|
||||
);
|
||||
const widgets = ["SolarFeaturedPostWidget", "SolarCheckInWidget"];
|
||||
for(final widget in widgets) {
|
||||
await HomeWidget.updateWidget(
|
||||
name: widget,
|
||||
iOSName: widget,
|
||||
androidName: "com.solsynth.solian.$widget",
|
||||
qualifiedAndroidName: "group.solsynth.solian.$widget",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:home_widget/home_widget.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
@ -428,7 +427,9 @@ class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendati
|
||||
setState(() => _isBusy = true);
|
||||
try {
|
||||
final pt = context.read<SnPostContentProvider>();
|
||||
final home = context.read<HomeWidgetProvider>();
|
||||
_posts = await pt.listRecommendations();
|
||||
home.saveWidgetData('post_featured', _posts!.map((e) => e.toJson()).toList());
|
||||
} catch (err) {
|
||||
if (!mounted) return;
|
||||
context.showErrorDialog(err);
|
||||
|
@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
# In Windows, build-name is used as the major, minor, and patch parts
|
||||
# of the product and file versions while build-number is used as the build suffix.
|
||||
version: 2.0.1+25
|
||||
version: 2.0.1+27
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
|
Loading…
Reference in New Issue
Block a user