This commit is contained in:
otsmr 2025-07-18 15:13:17 +02:00
parent afb1806cb1
commit c18a8ed311
16 changed files with 194 additions and 10 deletions

View file

@ -1,6 +1,6 @@
# Changelog
## 0.0.58 (WIP)
## 0.0.58
- twonly now has a free plan and is now financed by donations and an optional subscription with more features (coming soon)
- Implementing iOS gestures to close images

View file

@ -1,5 +1,9 @@
import 'dart:async';
import 'dart:io';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
@ -12,6 +16,7 @@ import 'package:twonly/src/views/components/app_outdated.dart';
import 'package:twonly/src/views/home.view.dart';
import 'package:twonly/src/views/onboarding/onboarding.view.dart';
import 'package:twonly/src/views/onboarding/register.view.dart';
import 'package:twonly/src/views/settings/help/changelog.view.dart';
class App extends StatefulWidget {
const App({super.key});

View file

@ -330,5 +330,6 @@
"appOutdatedBtn": "Jetzt aktualisieren.",
"doubleClickToReopen": "Doppelklicken zum\nerneuten Öffnen.",
"retransmissionRequested": "Wird erneut versucht.",
"testPaymentMethod": "Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!"
"testPaymentMethod": "Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!",
"openChangeLog": "Changelog automatisch öffnen"
}

View file

@ -486,5 +486,6 @@
"appOutdatedBtn": "Update Now",
"doubleClickToReopen": "Double-click\nto open again",
"retransmissionRequested": "Retransmission requested",
"testPaymentMethod": "Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!"
"testPaymentMethod": "Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!",
"openChangeLog": "Open changelog automatically"
}

View file

@ -2023,6 +2023,12 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!'**
String get testPaymentMethod;
/// No description provided for @openChangeLog.
///
/// In en, this message translates to:
/// **'Open changelog automatically'**
String get openChangeLog;
}
class _AppLocalizationsDelegate

View file

@ -1073,4 +1073,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get testPaymentMethod =>
'Vielen Dank für dein Interesse an einem kostenpflichtigen Tarif. Die kostenpflichtigen Pläne sind derzeit noch deaktiviert. Sie werden aber bald aktiviert!';
@override
String get openChangeLog => 'Changelog automatisch öffnen';
}

View file

@ -1067,4 +1067,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get testPaymentMethod =>
'Thanks for the interest in a paid plan. Currently the paid plans are still deactivated. But they will be activated soon!';
@override
String get openChangeLog => 'Open changelog automatically';
}

View file

@ -77,6 +77,9 @@ class UserData {
List<int>? lastChangeLogHash;
@JsonKey(defaultValue: false)
bool hideChangeLog = false;
// --- BACKUP ---
DateTime? nextTimeToShowBackupNotice;

View file

@ -56,6 +56,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..lastChangeLogHash = (json['lastChangeLogHash'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList()
..hideChangeLog = json['hideChangeLog'] as bool? ?? false
..nextTimeToShowBackupNotice = json['nextTimeToShowBackupNotice'] == null
? null
: DateTime.parse(json['nextTimeToShowBackupNotice'] as String)
@ -95,6 +96,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash,
'hideChangeLog': instance.hideChangeLog,
'nextTimeToShowBackupNotice':
instance.nextTimeToShowBackupNotice?.toIso8601String(),
'backupServer': instance.backupServer,

View file

@ -1,6 +1,9 @@
import 'dart:async';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart';
import 'package:twonly/globals.dart';
@ -24,6 +27,7 @@ import 'package:twonly/src/views/components/initialsavatar.dart';
import 'package:twonly/src/views/components/message_send_state_icon.dart';
import 'package:twonly/src/views/components/notification_badge.dart';
import 'package:twonly/src/views/components/user_context_menu.dart';
import 'package:twonly/src/views/settings/help/changelog.view.dart';
import 'package:twonly/src/views/settings/help/contact_us.view.dart';
import 'package:twonly/src/views/settings/settings_main.view.dart';
import 'package:twonly/src/views/settings/subscription/subscription.view.dart';
@ -71,10 +75,26 @@ class _ChatListViewState extends State<ChatListView> {
});
final user = await getUser();
if (user != null) {
if (user == null) return;
setState(() {
showFeedbackShortcut = user.showFeedbackShortcut;
});
final changeLog = await rootBundle.loadString('CHANGELOG.md');
final changeLogHash =
(await compute(Sha256().hash, changeLog.codeUnits)).bytes;
if (!user.hideChangeLog &&
user.lastChangeLogHash.toString() != changeLogHash.toString()) {
await updateUserdata((u) {
u.lastChangeLogHash = changeLogHash;
return u;
});
if (!mounted) return;
await Navigator.push(context, MaterialPageRoute(builder: (context) {
return ChangeLogView(
changeLog: changeLog,
);
}));
}
}

View file

@ -108,7 +108,7 @@ class _AppearanceViewState extends State<AppearanceView> {
ListTile(
title: Text(context.lang.contactUsShortcut),
onTap: toggleShowFeedbackIcon,
trailing: Checkbox(
trailing: Switch(
value: !showFeedbackShortcut,
onChanged: (a) => toggleShowFeedbackIcon(),
),

View file

@ -68,7 +68,7 @@ class _DataAndStorageViewState extends State<DataAndStorageView> {
title: Text(context.lang.settingsStorageDataStoreInGTitle),
subtitle: Text(context.lang.settingsStorageDataStoreInGSubtitle),
onTap: toggleStoreInGallery,
trailing: Checkbox(
trailing: Switch(
value: storeMediaFilesInGallery,
onChanged: (a) => toggleStoreInGallery(),
),

View file

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
List<Widget> parseMarkdown(BuildContext context, String markdown) {
List<Widget> widgets = [];
// Split the string into lines
final lines = markdown.split('\n');
for (var line in lines) {
line = line.trim();
// Check for headers
if (line.startsWith('# ')) {
// widgets.add(Padding(
// padding: const EdgeInsets.all(8),
// child: Text(
// line.substring(2), // Remove the '# ' part
// textAlign: TextAlign.center,
// style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
// ),
// ));
} else if (line.startsWith('## ')) {
widgets.add(Padding(
padding: const EdgeInsets.only(top: 20, bottom: 10),
child: Text(
line.substring(3), // Remove the '## ' part
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
));
}
// Check for bullet points
else if (line.startsWith('- ')) {
widgets.add(Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.only(top: 7),
child: Icon(
Icons.brightness_1,
size: 7,
color: context.color.onSurface,
),
), // Bullet point icon
const SizedBox(width: 8), // Space between bullet and text
Expanded(child: Text(line.substring(2))), // Remove the '- ' part
],
));
} else {
widgets.add(Text(line));
}
}
return widgets;
}
class ChangeLogView extends StatefulWidget {
const ChangeLogView({super.key, this.changeLog});
final String? changeLog;
@override
State<ChangeLogView> createState() => _ChangeLogViewState();
}
class _ChangeLogViewState extends State<ChangeLogView> {
String changeLog = '';
bool hideChangeLog = false;
@override
void initState() {
super.initState();
if (widget.changeLog != null) {
changeLog = widget.changeLog!;
} else {
initAsync();
}
}
Future<void> initAsync() async {
changeLog = await rootBundle.loadString('CHANGELOG.md');
final user = await getUser();
if (user != null) {
hideChangeLog = user.hideChangeLog;
}
setState(() {});
}
Future<void> _toggleAutoOpen(bool value) async {
await updateUserdata((u) {
u.hideChangeLog = !hideChangeLog;
return u;
});
setState(() {
hideChangeLog = !value;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Changelog'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8),
child: ListView(
children: parseMarkdown(context, changeLog),
),
),
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(context.lang.openChangeLog),
Switch(
value: !hideChangeLog,
onChanged: _toggleAutoOpen,
),
],
),
),
);
}
}

View file

@ -5,6 +5,7 @@ import 'package:twonly/globals.dart';
import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.dart';
import 'package:twonly/src/views/settings/help/changelog.view.dart';
import 'package:twonly/src/views/settings/help/contact_us.view.dart';
import 'package:twonly/src/views/settings/help/credits.view.dart';
import 'package:twonly/src/views/settings/help/diagnostics.view.dart';
@ -91,6 +92,15 @@ class HelpView extends StatelessWidget {
}));
},
),
ListTile(
title: const Text('Changelog'),
onTap: () async {
await Navigator.push(context,
MaterialPageRoute(builder: (context) {
return const ChangeLogView();
}));
},
),
ListTile(
title: const Text("Open Source"),
onTap: () {

View file

@ -87,8 +87,8 @@ class _ManageSubscriptionViewState extends State<ManageSubscriptionView> {
style: const TextStyle(fontSize: 12),
),
onTap: toggleRenewalOption,
trailing: Checkbox(
value: autoRenewal,
trailing: Switch(
value: autoRenewal!,
onChanged: (a) {
toggleRenewalOption();
},

View file

@ -104,4 +104,5 @@ flutter:
- assets/animated_icons/
- assets/animations/
- assets/passwords/
- CHANGELOG.md