Compare commits

...

5 Commits

Author SHA1 Message Date
b50191970e 🚀 Launch 2.1.1+35 2024-12-21 23:30:59 +08:00
1b69e6dd42 📝 Remove todo 2024-12-21 23:26:58 +08:00
39fb4d474f App updates & web deeplink 2024-12-21 23:26:42 +08:00
392aebcad7 🐛 Fix android widgets 2024-12-21 22:55:35 +08:00
e9e3a4c474 🐛 Bug fixes on iOS widget 2024-12-21 22:38:22 +08:00
15 changed files with 274 additions and 148 deletions

View File

@ -33,22 +33,6 @@
</intent-filter> </intent-filter>
<!-- Sharing Intents --> <!-- Sharing Intents -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="sn.solsynth.dev"
android:pathPrefix="/invite"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:mimeType="*/*"
android:scheme="content" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />

View File

@ -6,6 +6,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId import androidx.glance.GlanceId
import androidx.glance.GlanceModifier import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme
import androidx.glance.action.clickable import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent import androidx.glance.appwidget.provideContent
@ -25,8 +26,10 @@ import androidx.glance.text.Text
import androidx.glance.text.TextStyle import androidx.glance.text.TextStyle
import com.google.gson.FieldNamingPolicy import com.google.gson.FieldNamingPolicy
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import dev.solsynth.solian.MainActivity
import dev.solsynth.solian.data.InstantAdapter import dev.solsynth.solian.data.InstantAdapter
import dev.solsynth.solian.data.SolarCheckInRecord import dev.solsynth.solian.data.SolarCheckInRecord
import es.antonborri.home_widget.actionStartActivity
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.OffsetDateTime import java.time.OffsetDateTime
@ -39,7 +42,9 @@ class CheckInWidget : GlanceAppWidget() {
override suspend fun provideGlance(context: Context, id: GlanceId) { override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent { provideContent {
GlanceContent(context, currentState()) GlanceTheme {
GlanceContent(context, currentState())
}
} }
} }
@ -53,18 +58,27 @@ class CheckInWidget : GlanceAppWidget() {
val resultTierSymbols = listOf("大凶", "", "中平", "", "大吉") val resultTierSymbols = listOf("大凶", "", "中平", "", "大吉")
val prefs = currentState.preferences val prefs = currentState.preferences
val checkInRaw = prefs.getString("pas_check_in_record", null) val checkInRaw: String? = prefs.getString("pas_check_in_record", null)
val checkIn: SolarCheckInRecord? =
checkInRaw?.let { checkInRaw ->
gson.fromJson(checkInRaw, SolarCheckInRecord::class.java)
} ?: null;
Column( Column(
modifier = GlanceModifier modifier = GlanceModifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
.background(Color.White) .background(GlanceTheme.colors.widgetBackground)
.padding(16.dp) .padding(16.dp)
.clickable(
onClick = actionStartActivity<MainActivity>(
context,
Uri.parse("https://sn.solsynth.dev")
)
)
) { ) {
if (checkInRaw != null) { if (checkIn != null) {
val checkIn: SolarCheckInRecord =
gson.fromJson(checkInRaw, SolarCheckInRecord::class.java)
val dateFormatter = DateTimeFormatter.ofPattern("EEE, MM/dd") val dateFormatter = DateTimeFormatter.ofPattern("EEE, MM/dd")
val checkDate = checkIn.createdAt.atZone(ZoneId.of("UTC")).toLocalDate() val checkDate = checkIn.createdAt.atZone(ZoneId.of("UTC")).toLocalDate()
@ -73,11 +87,11 @@ class CheckInWidget : GlanceAppWidget() {
Column { Column {
Text( Text(
text = resultTierSymbols[checkIn.resultTier], text = resultTierSymbols[checkIn.resultTier],
style = TextStyle(fontSize = 25.sp, fontFamily = FontFamily.Serif) style = TextStyle(fontSize = 17.sp)
) )
Text( Text(
text = "+${checkIn.resultExperience} EXP", text = "+${checkIn.resultExperience} EXP",
style = TextStyle(fontSize = 15.sp, fontFamily = FontFamily.Monospace) style = TextStyle(fontSize = 13.sp, fontFamily = FontFamily.Monospace)
) )
} }
Spacer(modifier = GlanceModifier.height(8.dp)) Spacer(modifier = GlanceModifier.height(8.dp))
@ -88,18 +102,18 @@ class CheckInWidget : GlanceAppWidget() {
ZoneId.systemDefault() ZoneId.systemDefault()
) )
.format(dateFormatter), .format(dateFormatter),
style = TextStyle(fontSize = 13.sp) style = TextStyle(fontSize = 11.sp)
) )
} }
return@Column; return@Column;
} }
} }
}
Text( Text(
text = "You haven't checked in today", text = "You haven't checked in today",
style = TextStyle(fontSize = 15.sp) style = TextStyle(fontSize = 15.sp)
) )
}
} }
} }

View File

@ -1,6 +1,6 @@
import HomeWidgetGlanceState
import HomeWidgetGlanceStateDefinition
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -9,17 +9,13 @@ import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId import androidx.glance.GlanceId
import androidx.glance.GlanceModifier import androidx.glance.GlanceModifier
import androidx.glance.GlanceTheme import androidx.glance.GlanceTheme
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.action.clickable import androidx.glance.action.clickable
import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.cornerRadius
import androidx.glance.appwidget.provideContent import androidx.glance.appwidget.provideContent
import androidx.glance.background import androidx.glance.background
import androidx.glance.currentState import androidx.glance.currentState
import androidx.glance.layout.Alignment import androidx.glance.layout.Alignment
import androidx.glance.layout.Column import androidx.glance.layout.Column
import androidx.glance.layout.ContentScale
import androidx.glance.layout.Row import androidx.glance.layout.Row
import androidx.glance.layout.Spacer import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxHeight import androidx.glance.layout.fillMaxHeight
@ -39,10 +35,6 @@ import dev.solsynth.solian.MainActivity
import dev.solsynth.solian.data.InstantAdapter import dev.solsynth.solian.data.InstantAdapter
import dev.solsynth.solian.data.SolarPost import dev.solsynth.solian.data.SolarPost
import es.antonborri.home_widget.actionStartActivity import es.antonborri.home_widget.actionStartActivity
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import java.time.Instant import java.time.Instant
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
@ -52,45 +44,18 @@ class RandomPostWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>? override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition() get() = HomeWidgetGlanceStateDefinition()
private val defaultUrl = "https://api.sn.solsynth.dev"
override suspend fun provideGlance(context: Context, id: GlanceId) { override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent { provideContent {
GlanceTheme { GlanceTheme {
GlanceContent(context, currentState(), null) GlanceContent(context, currentState())
} }
} }
} }
private val client = OkHttpClient()
private fun resizeBitmap(bitmap: Bitmap, maxWidth: Int, maxHeight: Int): Bitmap {
val aspectRatio = bitmap.width.toFloat() / bitmap.height.toFloat()
val newWidth = if (bitmap.width > maxWidth) maxWidth else bitmap.width
val newHeight = (newWidth / aspectRatio).toInt()
val resizedBitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
return resizedBitmap
}
private fun loadImageFromUrl(url: String): Bitmap? {
val request = Request.Builder().url(url).build()
return try {
val response: Response = client.newCall(request).execute()
val inputStream = response.body?.byteStream()
val bitmap = BitmapFactory.decodeStream(inputStream)
resizeBitmap(bitmap, 120, 120)
} catch (e: IOException) {
e.printStackTrace()
null
}
}
@Composable @Composable
private fun GlanceContent( private fun GlanceContent(
context: Context, context: Context,
currentState: HomeWidgetGlanceState, currentState: HomeWidgetGlanceState,
avatar: Bitmap?
) { ) {
val prefs = currentState.preferences val prefs = currentState.preferences
val postRaw = prefs.getString("int_random_post", null) val postRaw = prefs.getString("int_random_post", null)
@ -109,7 +74,7 @@ class RandomPostWidget : GlanceAppWidget() {
modifier = GlanceModifier modifier = GlanceModifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
.background(Color.White) .background(GlanceTheme.colors.widgetBackground)
.padding(16.dp) .padding(16.dp)
.clickable( .clickable(
onClick = actionStartActivity<MainActivity>( onClick = actionStartActivity<MainActivity>(
@ -120,17 +85,6 @@ class RandomPostWidget : GlanceAppWidget() {
) { ) {
if (data != null) { if (data != null) {
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
if (avatar != null) {
Image(
provider = ImageProvider(bitmap = avatar),
contentDescription = null,
modifier = GlanceModifier.width(36.dp).height(36.dp)
.cornerRadius(18.dp),
contentScale = ContentScale.Crop
)
Spacer(modifier = GlanceModifier.width(8.dp))
}
Text( Text(
text = data.publisher.nick, text = data.publisher.nick,
style = TextStyle(fontSize = 15.sp) style = TextStyle(fontSize = 15.sp)

View File

@ -1,6 +1,6 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/glance_default_loading_layout" android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="120dp" android:minWidth="40dp"
android:minHeight="40dp" android:minHeight="40dp"
android:resizeMode="horizontal|vertical" android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="10000"> android:updatePeriodMillis="10000">

View File

@ -370,6 +370,8 @@
"dailyCheckNegativeHint6": "Going out", "dailyCheckNegativeHint6": "Going out",
"dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain", "dailyCheckNegativeHint6Description": "Forgot your umbrella and got caught in the rain",
"happyBirthday": "Happy birthday, {}!", "happyBirthday": "Happy birthday, {}!",
"celebrateMerryXmas": "Merry christmas, {}",
"celebrateNewYear": "Happy new year, {}",
"friendNew": "Add Friend", "friendNew": "Add Friend",
"friendRequests": "Friend Requests", "friendRequests": "Friend Requests",
"friendRequestsDescription": { "friendRequestsDescription": {
@ -455,5 +457,7 @@
"poweredBy": "Powered by {}", "poweredBy": "Powered by {}",
"shareIntent": "Share", "shareIntent": "Share",
"shareIntentDescription": "What do you want to do with the content you are sharing?", "shareIntentDescription": "What do you want to do with the content you are sharing?",
"shareIntentPostStory": "Post a Story" "shareIntentPostStory": "Post a Story",
"updateAvailable": "Update Available",
"updateOngoing": "正在更新,请稍后..."
} }

View File

@ -368,6 +368,8 @@
"dailyCheckNegativeHint6": "出门", "dailyCheckNegativeHint6": "出门",
"dailyCheckNegativeHint6Description": "忘带伞遇上大雨", "dailyCheckNegativeHint6Description": "忘带伞遇上大雨",
"happyBirthday": "生日快乐,{}", "happyBirthday": "生日快乐,{}",
"celebrateMerryXmas": "圣诞快乐,{}",
"celebrateNewYear": "新年快乐,{}",
"friendNew": "添加好友", "friendNew": "添加好友",
"friendRequests": "好友请求", "friendRequests": "好友请求",
"friendRequestsDescription": { "friendRequestsDescription": {
@ -453,5 +455,7 @@
"poweredBy": "由 {} 提供支持", "poweredBy": "由 {} 提供支持",
"shareIntent": "分享", "shareIntent": "分享",
"shareIntentDescription": "您想对您分享的内容做些什么?", "shareIntentDescription": "您想对您分享的内容做些什么?",
"shareIntentPostStory": "发布动态" "shareIntentPostStory": "发布动态",
"updateAvailable": "检测到更新可用",
"updateOngoing": "正在更新,请稍后……"
} }

View File

@ -10,7 +10,7 @@ import SwiftUI
struct CheckInProvider: TimelineProvider { struct CheckInProvider: TimelineProvider {
func placeholder(in context: Context) -> CheckInEntry { func placeholder(in context: Context) -> CheckInEntry {
CheckInEntry(date: Date(), user: nil, checkIn: nil) CheckInEntry(date: Date(), checkIn: nil)
} }
func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) { func getSnapshot(in context: Context, completion: @escaping (CheckInEntry) -> ()) {
@ -23,24 +23,17 @@ struct CheckInProvider: TimelineProvider {
jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter) jsonDecoder.dateDecodingStrategy = .formatted(dateFormatter)
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase 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 checkInRaw = prefs?.string(forKey: "pas_check_in_record") let checkInRaw = prefs?.string(forKey: "pas_check_in_record")
var checkIn: SolarCheckInRecord? var checkIn: SolarCheckInRecord?
if let checkInRaw = checkInRaw { if let checkInRaw = checkInRaw {
checkIn = try! jsonDecoder.decode(SolarCheckInRecord.self, from: checkInRaw.data(using: .utf8)!) checkIn = try! jsonDecoder.decode(SolarCheckInRecord.self, from: checkInRaw.data(using: .utf8)!)
if checkIn != nil && Calendar.current.isDate(checkIn!.createdAt, inSameDayAs: Date()) { if checkIn != nil && !Calendar.current.isDate(checkIn!.createdAt, inSameDayAs: Date()) {
checkIn = nil checkIn = nil
} }
} }
let entry = CheckInEntry( let entry = CheckInEntry(
date: Date(), date: Date(),
user: user,
checkIn: checkIn checkIn: checkIn
) )
completion(entry) completion(entry)
@ -56,7 +49,6 @@ struct CheckInProvider: TimelineProvider {
struct CheckInEntry: TimelineEntry { struct CheckInEntry: TimelineEntry {
let date: Date let date: Date
let user: SolarUser?
let checkIn: SolarCheckInRecord? let checkIn: SolarCheckInRecord?
} }
@ -135,10 +127,9 @@ struct CheckInWidget: Widget {
#Preview(as: .systemSmall) { #Preview(as: .systemSmall) {
CheckInWidget() CheckInWidget()
} timeline: { } timeline: {
CheckInEntry(date: .now, user: nil, checkIn: nil) CheckInEntry(date: .now, checkIn: nil)
CheckInEntry( CheckInEntry(
date: .now, date: .now,
user: SolarUser(id: 1, name: "demo", nick: "Deemo"),
checkIn: SolarCheckInRecord(id: 1, resultTier: 1, resultExperience: 100, createdAt: Date.now) checkIn: SolarCheckInRecord(id: 1, resultTier: 1, resultExperience: 100, createdAt: Date.now)
) )
} }

View File

@ -13,7 +13,7 @@ struct RandomPostProvider: TimelineProvider {
func placeholder(in context: Context) -> RandomPostEntry { func placeholder(in context: Context) -> RandomPostEntry {
RandomPostEntry(date: Date(), user: nil, randomPost: nil, family: .systemMedium) RandomPostEntry(date: Date(), user: nil, randomPost: nil, family: .systemMedium)
} }
func getSnapshot(in context: Context, completion: @escaping (RandomPostEntry) -> ()) { func getSnapshot(in context: Context, completion: @escaping (RandomPostEntry) -> ()) {
let prefs = UserDefaults(suiteName: "group.solsynth.solian") let prefs = UserDefaults(suiteName: "group.solsynth.solian")
@ -45,7 +45,7 @@ struct RandomPostProvider: TimelineProvider {
) )
completion(entry) completion(entry)
} }
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) { func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
getSnapshot(in: context) { (entry) in getSnapshot(in: context) { (entry) in
let timeline = Timeline(entries: [entry], policy: .atEnd) let timeline = Timeline(entries: [entry], policy: .atEnd)
@ -64,7 +64,7 @@ struct RandomPostEntry: TimelineEntry {
struct RandomPostWidgetEntryView : View { struct RandomPostWidgetEntryView : View {
var entry: RandomPostProvider.Entry var entry: RandomPostProvider.Entry
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
if let randomPost = entry.randomPost { if let randomPost = entry.randomPost {
@ -73,15 +73,20 @@ struct RandomPostWidgetEntryView : View {
if let avatar = randomPost.publisher.avatar { if let avatar = randomPost.publisher.avatar {
let avatarUrl = getAttachmentUrl(for: avatar) let avatarUrl = getAttachmentUrl(for: avatar)
let size: CGFloat = 28 let size: CGFloat = 28
let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: size, height: size), mode: .aspectFit) let scaleProcessor = ResizingImageProcessor(referenceSize: CGSize(width: size, height: size), mode: .aspectFill)
KFImage.url(URL(string: avatarUrl)) KFImage.url(URL(string: avatarUrl))
.resizable() .resizable()
.setProcessor(scaleProcessor) .setProcessor(scaleProcessor)
.fade(duration: 0.25) .fade(duration: 0.25)
.aspectRatio(contentMode: .fit) .placeholder{
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
}
.aspectRatio(contentMode: .fill)
.frame(width: size, height: size) .frame(width: size, height: size)
.cornerRadius(size / 2) .cornerRadius(size / 2)
.frame(width: size, height: size, alignment: .center) .frame(width: size, height: size, alignment: .center)
} }
@ -158,7 +163,7 @@ struct RandomPostWidgetEntryView : View {
struct RandomPostWidget: Widget { struct RandomPostWidget: Widget {
let kind: String = "SolarRandomPostWidget" let kind: String = "SolarRandomPostWidget"
var body: some WidgetConfiguration { var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: RandomPostProvider()) { entry in StaticConfiguration(kind: kind, provider: RandomPostProvider()) { entry in
if #available(iOS 17.0, *) { if #available(iOS 17.0, *) {

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:croppy/croppy.dart'; import 'package:croppy/croppy.dart';
import 'package:dio/dio.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:easy_localization_loader/easy_localization_loader.dart'; import 'package:easy_localization_loader/easy_localization_loader.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
@ -12,9 +13,11 @@ import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:relative_time/relative_time.dart'; import 'package:relative_time/relative_time.dart';
import 'package:responsive_framework/responsive_framework.dart'; import 'package:responsive_framework/responsive_framework.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:surface/firebase_options.dart'; import 'package:surface/firebase_options.dart';
import 'package:surface/providers/channel.dart'; import 'package:surface/providers/channel.dart';
@ -38,7 +41,9 @@ import 'package:surface/types/realm.dart';
import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy; import 'package:flutter_web_plugins/url_strategy.dart' show usePathUrlStrategy;
import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/dialog.dart';
import 'package:surface/widgets/version_label.dart'; import 'package:surface/widgets/version_label.dart';
import 'package:version/version.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
import 'package:in_app_review/in_app_review.dart';
@pragma('vm:entry-point') @pragma('vm:entry-point')
void appBackgroundDispatcher() { void appBackgroundDispatcher() {
@ -125,7 +130,7 @@ class SolianApp extends StatelessWidget {
Provider(create: (ctx) => HomeWidgetProvider(ctx)), Provider(create: (ctx) => HomeWidgetProvider(ctx)),
// Preferences layer // Preferences layer
Provider(create: (ctx) => ConfigProvider(ctx)), ChangeNotifierProvider(create: (ctx) => ConfigProvider(ctx)),
// Display layer // Display layer
ChangeNotifierProvider(create: (_) => ThemeProvider()), ChangeNotifierProvider(create: (_) => ThemeProvider()),
@ -201,6 +206,55 @@ class _AppSplashScreen extends StatefulWidget {
class _AppSplashScreenState extends State<_AppSplashScreen> { class _AppSplashScreenState extends State<_AppSplashScreen> {
bool _isReady = false; bool _isReady = false;
void _tryRequestRating() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.containsKey('first_boot_time')) {
final rawTime = prefs.getString('first_boot_time');
final time = DateTime.tryParse(rawTime ?? '');
if (time != null && time.isBefore(DateTime.now().subtract(const Duration(days: 3)))) {
final inAppReview = InAppReview.instance;
if (prefs.getBool('rating_requested') == true) return;
if (await inAppReview.isAvailable()) {
await inAppReview.requestReview();
prefs.setBool('rating_requested', true);
} else {
log('Unable request app review, unavailable');
}
}
} else {
prefs.setString('first_boot_time', DateTime.now().toIso8601String());
}
}
Future<void> _checkForUpdate() async {
if (kIsWeb) return;
try {
final info = await PackageInfo.fromPlatform();
final localVersionString = '${info.version}+${info.buildNumber}';
final resp = await Dio(
BaseOptions(
sendTimeout: const Duration(seconds: 60),
receiveTimeout: const Duration(seconds: 60),
),
).get(
'https://git.solsynth.dev/api/v1/repos/HyperNet/Surface/tags?page=1&limit=1',
);
final remoteVersionString = (resp.data as List).firstOrNull?['name'] ?? '0.0.0+0';
final remoteVersion = Version.parse(remoteVersionString.split('+').first);
final localVersion = Version.parse(localVersionString.split('+').first);
final remoteBuildNumber = int.tryParse(remoteVersionString.split('+').last) ?? 0;
final localBuildNumber = int.tryParse(localVersionString.split('+').last) ?? 0;
log("[Update] Local: $localVersionString, Remote: $remoteVersionString");
if ((remoteVersion > localVersion || remoteBuildNumber > localBuildNumber) && mounted) {
final config = context.read<ConfigProvider>();
config.setUpdate(remoteVersionString);
log("[Update] Update available: $remoteVersionString");
}
} catch (e) {
if (mounted) context.showErrorDialog('Unable to check update: $e');
}
}
Future<void> _initialize() async { Future<void> _initialize() async {
try { try {
final home = context.read<HomeWidgetProvider>(); final home = context.read<HomeWidgetProvider>();
@ -235,7 +289,11 @@ class _AppSplashScreenState extends State<_AppSplashScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initialize().then((_) => _postInitialization()); _initialize().then((_) {
_postInitialization();
_tryRequestRating();
_checkForUpdate();
});
} }
@override @override

View File

@ -16,7 +16,7 @@ const Map<String, FilterQuality> kImageQualityLevel = {
'settingsImageQualityHigh': FilterQuality.high, 'settingsImageQualityHigh': FilterQuality.high,
}; };
class ConfigProvider { class ConfigProvider extends ChangeNotifier {
late final SharedPreferences prefs; late final SharedPreferences prefs;
late final HomeWidgetProvider _home; late final HomeWidgetProvider _home;
@ -36,8 +36,16 @@ class ConfigProvider {
String get serverUrl { String get serverUrl {
return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault; return prefs.getString(kNetworkServerStoreKey) ?? kNetworkServerDefault;
} }
set serverUrl(String url) { set serverUrl(String url) {
prefs.setString(kNetworkServerStoreKey, url); prefs.setString(kNetworkServerStoreKey, url);
_home.saveWidgetData("nex_server_url", url); _home.saveWidgetData("nex_server_url", url);
} }
String? updatableVersion;
void setUpdate(String newVersion) {
updatableVersion = newVersion;
notifyListeners();
}
} }

View File

@ -1,7 +1,10 @@
import 'dart:io';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui'; import 'dart:ui';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_app_update/flutter_app_update.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -10,6 +13,7 @@ import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:surface/providers/config.dart';
import 'package:surface/providers/post.dart'; import 'package:surface/providers/post.dart';
import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/sn_network.dart';
import 'package:surface/providers/userinfo.dart'; import 'package:surface/providers/userinfo.dart';
@ -69,18 +73,15 @@ class _HomeScreenState extends State<HomeScreen> {
body: LayoutBuilder( body: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return Align( return Align(
alignment: constraints.maxWidth > 640 alignment: constraints.maxWidth > 640 ? Alignment.center : Alignment.topCenter,
? Alignment.center
: Alignment.topCenter,
child: Container( child: Container(
constraints: const BoxConstraints(maxWidth: 640), constraints: const BoxConstraints(maxWidth: 640),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
mainAxisAlignment: constraints.maxWidth > 640 mainAxisAlignment: constraints.maxWidth > 640 ? MainAxisAlignment.center : MainAxisAlignment.start,
? MainAxisAlignment.center
: MainAxisAlignment.start,
children: [ children: [
_HomeDashSpecialDayWidget().padding(top: 8, horizontal: 8), _HomeDashSpecialDayWidget().padding(bottom: 8, horizontal: 8),
_HomeDashUpdateWidget(padding: const EdgeInsets.only(bottom: 8, left: 8, right: 8)),
StaggeredGrid.extent( StaggeredGrid.extent(
maxCrossAxisExtent: 280, maxCrossAxisExtent: 280,
mainAxisSpacing: 8, mainAxisSpacing: 8,
@ -104,6 +105,52 @@ class _HomeScreenState extends State<HomeScreen> {
} }
} }
class _HomeDashUpdateWidget extends StatelessWidget {
final EdgeInsets? padding;
const _HomeDashUpdateWidget({super.key, this.padding});
@override
Widget build(BuildContext context) {
final config = context.watch<ConfigProvider>();
return ListenableBuilder(
listenable: config,
builder: (context, _) {
if (config.updatableVersion != null) {
return Container(
padding: padding,
child: Card(
child: ListTile(
leading: Icon(Symbols.update),
title: Text('updateAvailable').tr(),
subtitle: Text(config.updatableVersion!),
trailing: (kIsWeb || Platform.isWindows || Platform.isLinux)
? null
: IconButton(
icon: const Icon(Symbols.arrow_right_alt),
onPressed: () {
final model = UpdateModel(
'https://files.solsynth.dev/d/production01/solian/app-arm64-v8a-release.apk',
'solian-app-release-${config.updatableVersion!}.apk',
'ic_notification',
'https://apps.apple.com/us/app/solian/id6499032345',
);
AzhonAppUpdate.update(model);
context.showSnackbar('updateOngoing'.tr());
},
),
),
),
);
}
return SizedBox.shrink();
},
);
}
}
class _HomeDashSpecialDayWidget extends StatelessWidget { class _HomeDashSpecialDayWidget extends StatelessWidget {
const _HomeDashSpecialDayWidget({super.key}); const _HomeDashSpecialDayWidget({super.key});
@ -112,10 +159,10 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
final ua = context.watch<UserProvider>(); final ua = context.watch<UserProvider>();
final today = DateTime.now(); final today = DateTime.now();
final birthday = ua.user?.profile?.birthday?.toLocal(); final birthday = ua.user?.profile?.birthday?.toLocal();
final isBirthday = birthday != null && final isBirthday = birthday != null && birthday.day == today.day && birthday.month == today.month;
birthday.day == today.day &&
birthday.month == today.month;
return Column( return Column(
spacing: 8,
children: [ children: [
if (isBirthday) if (isBirthday)
Card( Card(
@ -124,6 +171,20 @@ class _HomeDashSpecialDayWidget extends StatelessWidget {
title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']), title: Text('happyBirthday').tr(args: [ua.user?.nick ?? 'user']),
), ),
).padding(bottom: 8), ).padding(bottom: 8),
if (today.month == 12 && today.day == 25)
Card(
child: ListTile(
leading: Text('🎄').fontSize(24),
title: Text('celebrateMerryXmas').tr(args: [ua.user?.nick ?? 'user']),
),
),
if (today.month == 1 && today.day == 1)
Card(
child: ListTile(
leading: Text('🎉').fontSize(24),
title: Text('celebrateNewYear').tr(args: [ua.user?.nick ?? 'user']),
),
),
], ],
); );
} }
@ -174,20 +235,15 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
} }
Widget _buildDetailChunk(int index, bool positive) { Widget _buildDetailChunk(int index, bool positive) {
final prefix = final prefix = positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint';
positive ? 'dailyCheckPositiveHint' : 'dailyCheckNegativeHint'; final mod = positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount;
final mod =
positive ? kSuggestionPositiveHintCount : kSuggestionNegativeHintCount;
final pos = math.max(1, _todayRecord!.resultModifiers[index] % mod); final pos = math.max(1, _todayRecord!.resultModifiers[index] % mod);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
prefix.tr(args: ['$prefix$pos'.tr()]), prefix.tr(args: ['$prefix$pos'.tr()]),
style: Theme.of(context) style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
), ),
Text( Text(
'$prefix${pos}Description', '$prefix${pos}Description',
@ -222,10 +278,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
else else
Text( Text(
'dailyCheckEverythingIsNegative', 'dailyCheckEverythingIsNegative',
style: Theme.of(context) style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
).tr(), ).tr(),
const Gap(8), const Gap(8),
if (_todayRecord?.resultTier != 4) if (_todayRecord?.resultTier != 4)
@ -241,10 +294,7 @@ class _HomeDashCheckInWidgetState extends State<_HomeDashCheckInWidget> {
else else
Text( Text(
'dailyCheckEverythingIsPositive', 'dailyCheckEverythingIsPositive',
style: Theme.of(context) style: Theme.of(context).textTheme.titleMedium!.copyWith(fontWeight: FontWeight.bold),
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
).tr(), ).tr(),
], ],
), ),
@ -362,12 +412,10 @@ class _HomeDashNotificationWidget extends StatefulWidget {
const _HomeDashNotificationWidget({super.key}); const _HomeDashNotificationWidget({super.key});
@override @override
State<_HomeDashNotificationWidget> createState() => State<_HomeDashNotificationWidget> createState() => _HomeDashNotificationWidgetState();
_HomeDashNotificationWidgetState();
} }
class _HomeDashNotificationWidgetState class _HomeDashNotificationWidgetState extends State<_HomeDashNotificationWidget> {
extends State<_HomeDashNotificationWidget> {
int? _count; int? _count;
Future<void> _fetchNotificationCount() async { Future<void> _fetchNotificationCount() async {
@ -404,9 +452,7 @@ class _HomeDashNotificationWidgetState
style: Theme.of(context).textTheme.titleLarge, style: Theme.of(context).textTheme.titleLarge,
).tr(), ).tr(),
Text( Text(
_count == null _count == null ? 'loading'.tr() : 'notificationUnreadCount'.plural(_count ?? 0),
? 'loading'.tr()
: 'notificationUnreadCount'.plural(_count ?? 0),
style: Theme.of(context).textTheme.bodyLarge, style: Theme.of(context).textTheme.bodyLarge,
), ),
], ],
@ -437,12 +483,10 @@ class _HomeDashRecommendationPostWidget extends StatefulWidget {
const _HomeDashRecommendationPostWidget({super.key}); const _HomeDashRecommendationPostWidget({super.key});
@override @override
State<_HomeDashRecommendationPostWidget> createState() => State<_HomeDashRecommendationPostWidget> createState() => _HomeDashRecommendationPostWidgetState();
_HomeDashRecommendationPostWidgetState();
} }
class _HomeDashRecommendationPostWidgetState class _HomeDashRecommendationPostWidgetState extends State<_HomeDashRecommendationPostWidget> {
extends State<_HomeDashRecommendationPostWidget> {
bool _isBusy = false; bool _isBusy = false;
List<SnPost>? _posts; List<SnPost>? _posts;
@ -491,8 +535,7 @@ class _HomeDashRecommendationPostWidgetState
).padding(horizontal: 18, top: 12, bottom: 8), ).padding(horizontal: 18, top: 12, bottom: 8),
Expanded( Expanded(
child: PageView.builder( child: PageView.builder(
scrollBehavior: scrollBehavior: ScrollConfiguration.of(context).copyWith(dragDevices: {
ScrollConfiguration.of(context).copyWith(dragDevices: {
PointerDeviceKind.mouse, PointerDeviceKind.mouse,
PointerDeviceKind.touch, PointerDeviceKind.touch,
}), }),
@ -505,8 +548,7 @@ class _HomeDashRecommendationPostWidgetState
showMenu: false, showMenu: false,
).padding(bottom: 8), ).padding(bottom: 8),
onTap: () { onTap: () {
GoRouter.of(context) GoRouter.of(context).pushNamed('postDetail', pathParameters: {
.pushNamed('postDetail', pathParameters: {
'slug': _posts![index].id.toString(), 'slug': _posts![index].id.toString(),
}); });
}, },

View File

@ -16,6 +16,7 @@ import firebase_messaging
import flutter_udid import flutter_udid
import flutter_webrtc import flutter_webrtc
import gal import gal
import in_app_review
import livekit_client import livekit_client
import media_kit_libs_macos_video import media_kit_libs_macos_video
import media_kit_video import media_kit_video
@ -41,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin")) LiveKitPlugin.register(with: registry.registrar(forPlugin: "LiveKitPlugin"))
MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin"))
MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin"))

View File

@ -627,6 +627,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.2" version: "4.5.2"
flutter_app_update:
dependency: "direct main"
description:
name: flutter_app_update
sha256: "09290240949c4651581cd6fc535e52d019e189e694d6019c56b5a56c2e69ba65"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
flutter_cache_manager: flutter_cache_manager:
dependency: transitive dependency: transitive
description: description:
@ -729,10 +737,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_udid name: flutter_udid
sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07" sha256: be464dc5b1fb7ee894f6a32d65c086ca5e177fdcf9375ac08d77495b98150f84
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.1"
flutter_web_plugins: flutter_web_plugins:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -962,6 +970,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.1+1" version: "0.2.1+1"
in_app_review:
dependency: "direct main"
description:
name: in_app_review
sha256: "36a06771b88fb0e79985b15e7f2ac0f1142e903fe72517f3c055d78bc3bc1819"
url: "https://pub.dev"
source: hosted
version: "2.0.10"
in_app_review_platform_interface:
dependency: transitive
description:
name: in_app_review_platform_interface
sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10
url: "https://pub.dev"
source: hosted
version: "2.0.5"
intl: intl:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1975,6 +1999,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
version:
dependency: "direct main"
description:
name: version
sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
very_good_infinite_list: very_good_infinite_list:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # 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: 2.1.1+36 version: 2.1.1+35
environment: environment:
sdk: ^3.5.4 sdk: ^3.5.4
@ -108,6 +108,9 @@ dependencies:
home_widget: ^0.7.0 home_widget: ^0.7.0
receive_sharing_intent: ^1.8.1 receive_sharing_intent: ^1.8.1
workmanager: ^0.5.2 workmanager: ^0.5.2
flutter_app_update: ^3.2.2
in_app_review: ^2.0.10
version: ^3.0.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -0,0 +1,25 @@
{
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"W7HPZ53V6B.dev.solsynth.solian"
],
"paths": [
"*"
],
"components": [
{
"/": "/*"
}
]
}
]
},
"webcredentials": {
"apps": [
"W7HPZ53V6B.dev.solsynth.solian"
]
}
}