Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
01b13b4764 |
|||
|
050e42cc6f |
|||
|
67cee201b6 |
|||
|
2d7b067c93 |
|||
|
a9993013f5 |
|||
|
9c488e8981 |
|||
|
5c16f42d20 |
|||
|
092fc616e6 |
|||
|
6cbf78be6b |
|||
|
68f4bb0c03 |
|||
|
9e24f4cb31 |
|||
|
43d2e94d62 |
|||
|
62fe37c6ae |
|||
|
b9dfbd3ca2 |
|||
|
620440e2a1 |
15
.github/workflows/macos.yml
vendored
|
|
@ -26,15 +26,22 @@ jobs:
|
||||||
with:
|
with:
|
||||||
go-version-file: gomuks/go.mod
|
go-version-file: gomuks/go.mod
|
||||||
|
|
||||||
- name: Build app
|
- name: Build App
|
||||||
run: |
|
run: |
|
||||||
flutter pub get
|
flutter pub get
|
||||||
dart scripts/generate.dart
|
dart scripts/generate.dart
|
||||||
flutter pub run build_runner build
|
flutter pub run build_runner build
|
||||||
flutter build macos --release
|
flutter build macos --release
|
||||||
|
|
||||||
- name: Upload installer artifact
|
- name: Create DMG
|
||||||
|
id: create-dmg
|
||||||
|
uses: L-Super/create-dmg-actions@28511e988b13ca34096d439159a66d468b8e724f
|
||||||
|
with:
|
||||||
|
dmg_name: nexus
|
||||||
|
src_dir: build/macos/Build/Products/Release/Nexus.app
|
||||||
|
|
||||||
|
- name: Upload DMG
|
||||||
uses: actions/upload-artifact@v6
|
uses: actions/upload-artifact@v6
|
||||||
with:
|
with:
|
||||||
name: Nexus.App
|
name: nexus.dmg
|
||||||
path: build/macos/Build/Products/Release/Nexus.app
|
path: ${{ steps.create-dmg.outputs.dmg_path }}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ A simple and user-friendly Matrix client made with Flutter and a Gomuks backend.
|
||||||
- [x] Linux
|
- [x] Linux
|
||||||
- [x] Windows - Currently has some bugs, due to us not having any developers on Windows. If you want to fix these, get in contact with us though the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus).
|
- [x] Windows - Currently has some bugs, due to us not having any developers on Windows. If you want to fix these, get in contact with us though the [Nexus Client Matrix room](https://matrix.to/#/#nexus:federated.nexus).
|
||||||
- [x] Android
|
- [x] Android
|
||||||
- [x] MacOS - Unsigned .App only
|
- [x] MacOS - Unsigned .dmg only
|
||||||
- [x] iOS - Unsigned .ipa only
|
- [x] iOS - Unsigned .ipa only
|
||||||
- [ ] Web (may not be possible)
|
- [ ] Web (may not be possible)
|
||||||
- [x] Login (via OAuth)
|
- [x] Login (via OAuth)
|
||||||
|
|
@ -117,7 +117,7 @@ If you want to try out Nexus, grab one of the following artifacts from CI:
|
||||||
- [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip)
|
- [Android APK](https://nightly.link/Henry-Hiles/nexus/workflows/android/main/APK.zip)
|
||||||
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
||||||
- [Unsigned iOS IPA](https://nightly.link/Henry-Hiles/nexus/workflows/ios/main/Nexus.ipa.zip)
|
- [Unsigned iOS IPA](https://nightly.link/Henry-Hiles/nexus/workflows/ios/main/Nexus.ipa.zip)
|
||||||
- [Unsigned MacOS App](https://nightly.link/Henry-Hiles/nexus/workflows/macos/main/Nexus.App.zip)
|
- [Unsigned MacOS DMG](https://nightly.link/Henry-Hiles/nexus/workflows/macos/main/nexus.dmg.zip)
|
||||||
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
- [Windows EXE](https://nightly.link/Henry-Hiles/nexus/workflows/windows/main/windows-installer.zip)
|
||||||
- Flatpak
|
- Flatpak
|
||||||
- [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip)
|
- [AArch64/Arm64](https://nightly.link/Henry-Hiles/nexus/workflows/flatpak/main/flatpak-aarch64.zip)
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,10 @@ Future<void> main(List<String> args) => build(args, (input, output) async {
|
||||||
break;
|
break;
|
||||||
case OS.macOS:
|
case OS.macOS:
|
||||||
libFileName = "libgomuks.dylib";
|
libFileName = "libgomuks.dylib";
|
||||||
extraEnv = {"SDKROOT": await getXCodeTool()};
|
extraEnv = {
|
||||||
|
"SDKROOT": await getXCodeTool(),
|
||||||
|
"MACOSX_DEPLOYMENT_TARGET": codeConfig.macOS.targetVersion.toString(),
|
||||||
|
};
|
||||||
break;
|
break;
|
||||||
case OS.windows:
|
case OS.windows:
|
||||||
libFileName = "libgomuks.dll";
|
libFileName = "libgomuks.dll";
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 712 B After Width: | Height: | Size: 846 B |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 5 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.8 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 7.5 KiB |
|
|
@ -38,6 +38,7 @@ import "package:nexus/models/requests/set_membership.dart";
|
||||||
import "package:nexus/models/requests/set_state.dart";
|
import "package:nexus/models/requests/set_state.dart";
|
||||||
import "package:nexus/models/requests/upload_media.dart";
|
import "package:nexus/models/requests/upload_media.dart";
|
||||||
import "package:nexus/models/room.dart";
|
import "package:nexus/models/room.dart";
|
||||||
|
import "package:nexus/models/room_summary.dart";
|
||||||
import "package:nexus/models/spec_versions_response.dart";
|
import "package:nexus/models/spec_versions_response.dart";
|
||||||
import "package:nexus/models/sync_data.dart";
|
import "package:nexus/models/sync_data.dart";
|
||||||
import "package:nexus/src/third_party/gomuks.g.dart";
|
import "package:nexus/src/third_party/gomuks.g.dart";
|
||||||
|
|
@ -213,10 +214,11 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> joinRoom(JoinRoomRequest request) async {
|
Future<String> joinRoom(JoinRoomRequest request) async =>
|
||||||
final response = await _sendCommand("join_room", request.toJson());
|
(await _sendCommand("join_room", request.toJson()))["room_id"];
|
||||||
return response["room_id"];
|
|
||||||
}
|
Future<RoomSummary> getRoomSummary(JoinRoomRequest request) async =>
|
||||||
|
.fromJson(await _sendCommand("get_room_summary", request.toJson()));
|
||||||
|
|
||||||
Future<void> leaveRoom(Room room) async {
|
Future<void> leaveRoom(Room room) async {
|
||||||
if (room.metadata == null) return;
|
if (room.metadata == null) return;
|
||||||
|
|
@ -311,7 +313,7 @@ class ClientController extends AsyncNotifier<int> {
|
||||||
Future<Uri?> discoverHomeserver(Uri homeserver) async {
|
Future<Uri?> discoverHomeserver(Uri homeserver) async {
|
||||||
try {
|
try {
|
||||||
final response = await _sendCommand("discover_homeserver", {
|
final response = await _sendCommand("discover_homeserver", {
|
||||||
"user_id": "@fake-user:${homeserver.host}",
|
"user_id": "@fake-user:${homeserver.authority}",
|
||||||
});
|
});
|
||||||
return Uri.parse(response["m.homeserver"]?["base_url"]);
|
return Uri.parse(response["m.homeserver"]?["base_url"]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -17,30 +17,27 @@ import "package:nexus/models/room.dart";
|
||||||
|
|
||||||
class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
final String roomId;
|
final String roomId;
|
||||||
Future<bool>? _loadingOlder;
|
|
||||||
RoomChatController(this.roomId);
|
RoomChatController(this.roomId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<IList<Event>?> build() async {
|
Future<IList<Event>?> build() async {
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
final initialRoom = ref.watch(
|
final room = ref.watch(
|
||||||
RoomsController.provider.select((rooms) => rooms[roomId]),
|
RoomsController.provider.select((rooms) => rooms[roomId]),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (initialRoom == null) return null;
|
if (room == null) return null;
|
||||||
Room room = initialRoom;
|
|
||||||
|
|
||||||
if (!room.hasFetchedState) {
|
if (!room.hasFetchedState) {
|
||||||
final state = await client.getRoomState(.new(roomId: roomId));
|
final state = await client.getRoomState(.new(roomId: roomId));
|
||||||
|
|
||||||
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
await ref.read(RoomsController.provider.notifier).addState(roomId, state);
|
||||||
room = ref.read(RoomsController.provider)[roomId] ?? room;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load one more page when the initial timeline is short.
|
// While there are under 20 events, try to load more
|
||||||
|
// until there's no more or the conditions are met.
|
||||||
if (room.hasMore && room.timeline.length < 20) {
|
if (room.hasMore && room.timeline.length < 20) {
|
||||||
await loadOlder();
|
loadOlder();
|
||||||
room = ref.read(RoomsController.provider)[roomId] ?? room;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return room.timeline
|
return room.timeline
|
||||||
|
|
@ -77,46 +74,47 @@ class RoomChatController extends AsyncNotifier<IList<Event>?> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<bool> loadOlder() => _loadingOlder ??= _loadOlder().whenComplete(() {
|
Future<bool> loadOlder() async {
|
||||||
_loadingOlder = null;
|
state = AsyncLoading();
|
||||||
});
|
final timelineKeys = ref
|
||||||
|
.read(RoomsController.provider.select((value) => value[roomId]))
|
||||||
Future<bool> _loadOlder() async {
|
?.timeline
|
||||||
final room = ref.read(RoomsController.provider)[roomId];
|
.keys;
|
||||||
if (room == null || !room.hasMore) return false;
|
|
||||||
|
|
||||||
final timelineKeys = room.timeline.keys;
|
|
||||||
final response = await ref
|
final response = await ref
|
||||||
.watch(ClientController.provider.notifier)
|
.read(ClientController.provider.notifier)
|
||||||
.paginate(
|
.paginate(
|
||||||
.new(
|
.new(
|
||||||
roomId: roomId,
|
roomId: roomId,
|
||||||
maxTimelineId: timelineKeys.isNotEmpty
|
maxTimelineId: timelineKeys?.isNotEmpty == true
|
||||||
? timelineKeys.reduce(min)
|
? timelineKeys?.reduce(min)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
ref
|
if (response.events.isEmpty) {
|
||||||
.watch(RoomsController.provider.notifier)
|
state = .data(state.value);
|
||||||
.update(
|
} else {
|
||||||
IMap({
|
ref
|
||||||
roomId: Room(
|
.read(RoomsController.provider.notifier)
|
||||||
events: IMap.fromIterable(
|
.update(
|
||||||
response.events.addAll(response.relatedEvents),
|
IMap({
|
||||||
keyMapper: (event) => event.rowId,
|
roomId: Room(
|
||||||
valueMapper: (event) => event,
|
events: IMap.fromIterable(
|
||||||
|
response.events.addAll(response.relatedEvents),
|
||||||
|
keyMapper: (event) => event.rowId,
|
||||||
|
valueMapper: (event) => event,
|
||||||
|
),
|
||||||
|
hasMore: response.hasMore,
|
||||||
|
timeline: IMap.fromIterable(
|
||||||
|
response.events,
|
||||||
|
keyMapper: (event) => event.timelineRowId,
|
||||||
|
valueMapper: (event) => event.rowId,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
hasMore: response.hasMore,
|
}),
|
||||||
timeline: IMap.fromIterable(
|
.new(),
|
||||||
response.events,
|
);
|
||||||
keyMapper: (event) => event.timelineRowId,
|
}
|
||||||
valueMapper: (event) => event.rowId,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
.new(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.hasMore;
|
return response.hasMore;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
lib/controllers/room_summary.dart
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/client.dart";
|
||||||
|
import "package:nexus/models/requests/join_room.dart";
|
||||||
|
import "package:nexus/models/room_summary.dart";
|
||||||
|
|
||||||
|
class RoomSummaryController extends AsyncNotifier<RoomSummary> {
|
||||||
|
final JoinRoomRequest request;
|
||||||
|
RoomSummaryController(this.request);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RoomSummary> build() =>
|
||||||
|
ref.watch(ClientController.provider.notifier).getRoomSummary(request);
|
||||||
|
|
||||||
|
static final provider = AsyncNotifierProvider.family
|
||||||
|
.autoDispose<RoomSummaryController, RoomSummary, JoinRoomRequest>(
|
||||||
|
RoomSummaryController.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -9,14 +9,14 @@ class SettingsController extends AsyncNotifier<Settings> {
|
||||||
final file = await ref.watch(SettingsFileController.provider.future);
|
final file = await ref.watch(SettingsFileController.provider.future);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Settings.fromJson(json.decode(await file.readAsString()));
|
return .fromJson(json.decode(await file.readAsString()));
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return Settings();
|
return .new();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> set(Settings settings) async {
|
Future<void> set(Settings settings) async {
|
||||||
state = AsyncData(settings);
|
state = .data(settings);
|
||||||
final file = await ref.watch(SettingsFileController.provider.future);
|
final file = await ref.watch(SettingsFileController.provider.future);
|
||||||
await file.writeAsString(json.encode(settings.toJson()));
|
await file.writeAsString(json.encode(settings.toJson()));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ extension LinkToMention on String {
|
||||||
///
|
///
|
||||||
/// Returns the decoded identifier (e.g. "#room:matrix.org")
|
/// Returns the decoded identifier (e.g. "#room:matrix.org")
|
||||||
/// or null if this is not a Matrix link.
|
/// or null if this is not a Matrix link.
|
||||||
|
/// TODO: Needs to be reworked to handle event links. Might be worth just rewriting, I don't like this code.
|
||||||
String? get mention {
|
String? get mention {
|
||||||
final trimmed = trim();
|
final trimmed = trim();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part "join_room.g.dart";
|
||||||
abstract class JoinRoomRequest with _$JoinRoomRequest {
|
abstract class JoinRoomRequest with _$JoinRoomRequest {
|
||||||
const factory JoinRoomRequest({
|
const factory JoinRoomRequest({
|
||||||
required String roomIdOrAlias,
|
required String roomIdOrAlias,
|
||||||
required IList<String> via,
|
@Default(IList.empty()) IList<String> via,
|
||||||
}) = _JoinRoomRequest;
|
}) = _JoinRoomRequest;
|
||||||
|
|
||||||
factory JoinRoomRequest.fromJson(Map<String, Object?> json) =>
|
factory JoinRoomRequest.fromJson(Map<String, Object?> json) =>
|
||||||
|
|
|
||||||
23
lib/models/room_summary.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import "package:freezed_annotation/freezed_annotation.dart";
|
||||||
|
import "package:nexus/models/content/create.dart";
|
||||||
|
import "package:nexus/models/join_rule.dart";
|
||||||
|
part "room_summary.freezed.dart";
|
||||||
|
part "room_summary.g.dart";
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class RoomSummary with _$RoomSummary {
|
||||||
|
const factory RoomSummary({
|
||||||
|
required String roomId,
|
||||||
|
@JsonKey(name: "num_joined_members") required int joinedMembers,
|
||||||
|
JoinRule? joinRule,
|
||||||
|
String? name,
|
||||||
|
Uri? avatarUrl,
|
||||||
|
String? canonicalAlias,
|
||||||
|
String? topic,
|
||||||
|
String? roomVersion,
|
||||||
|
@JsonKey(unknownEnumValue: RoomType.room) RoomType? roomType,
|
||||||
|
}) = _RoomSummary;
|
||||||
|
|
||||||
|
factory RoomSummary.fromJson(Map<String, Object?> json) =>
|
||||||
|
_$RoomSummaryFromJson(json);
|
||||||
|
}
|
||||||
|
|
@ -26,6 +26,17 @@ class SelectServerPage extends HookConsumerWidget {
|
||||||
final homeserverUrl = useTextEditingController();
|
final homeserverUrl = useTextEditingController();
|
||||||
|
|
||||||
Future<void> setHomeserver(Uri? newHomeserver) async {
|
Future<void> setHomeserver(Uri? newHomeserver) async {
|
||||||
|
if (newHomeserver == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Failed to parse homeserver URL. Are you sure you typed it correctly?",
|
||||||
|
style: .new(color: theme.colorScheme.onErrorContainer),
|
||||||
|
),
|
||||||
|
backgroundColor: theme.colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -33,26 +44,14 @@ class SelectServerPage extends HookConsumerWidget {
|
||||||
newHomeserver = Uri.https(newHomeserver!.path);
|
newHomeserver = Uri.https(newHomeserver!.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
final newUrl = newHomeserver == null
|
final newUrl = await ref
|
||||||
? null
|
.read(ClientController.provider.notifier)
|
||||||
: await ref
|
.discoverHomeserver(newHomeserver!);
|
||||||
.read(ClientController.provider.notifier)
|
|
||||||
.discoverHomeserver(newHomeserver);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
if (newUrl == null) {
|
Future<void> tryLogin(Uri url) async {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
"Homeserver verification failed. Is your homeserver down?",
|
|
||||||
style: .new(color: theme.colorScheme.onErrorContainer),
|
|
||||||
),
|
|
||||||
backgroundColor: theme.colorScheme.errorContainer,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final codeResponse = await ref.watch(
|
final codeResponse = await ref.watch(
|
||||||
AuthUrlController.provider(newUrl).future,
|
AuthUrlController.provider(url).future,
|
||||||
);
|
);
|
||||||
|
|
||||||
await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url);
|
await ref.watch(LaunchHelper.provider).launchUrl(codeResponse.url);
|
||||||
|
|
@ -70,7 +69,7 @@ class SelectServerPage extends HookConsumerWidget {
|
||||||
.watch(ClientController.provider.notifier)
|
.watch(ClientController.provider.notifier)
|
||||||
.exchangeToken(
|
.exchangeToken(
|
||||||
.new(
|
.new(
|
||||||
homeserverUrl: newUrl,
|
homeserverUrl: url,
|
||||||
codeVerifier: codeResponse.codeVerifier,
|
codeVerifier: codeResponse.codeVerifier,
|
||||||
redirectUri: .new(
|
redirectUri: .new(
|
||||||
scheme: "nexus.federated.nexus",
|
scheme: "nexus.federated.nexus",
|
||||||
|
|
@ -78,7 +77,7 @@ class SelectServerPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
code: code,
|
code: code,
|
||||||
clientId: await ref.watch(
|
clientId: await ref.watch(
|
||||||
ClientIdController.provider(newUrl).future,
|
ClientIdController.provider(url).future,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -89,6 +88,25 @@ class SelectServerPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newUrl == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Homeserver verification failed.",
|
||||||
|
style: .new(color: theme.colorScheme.onErrorContainer),
|
||||||
|
),
|
||||||
|
action: SnackBarAction(
|
||||||
|
onPressed: () => tryLogin(newHomeserver!),
|
||||||
|
label: "Attempt log in anyways",
|
||||||
|
textColor: theme.colorScheme.onErrorContainer,
|
||||||
|
),
|
||||||
|
backgroundColor: theme.colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tryLogin(newUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
showError(error, stackTrace);
|
showError(error, stackTrace);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
import "package:flutter/material.dart";
|
import "package:flutter/material.dart";
|
||||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||||
|
import "package:nexus/controllers/room_summary.dart";
|
||||||
import "package:nexus/controllers/user.dart";
|
import "package:nexus/controllers/user.dart";
|
||||||
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
import "package:nexus/helpers/extensions/link_to_mention.dart";
|
||||||
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
import "package:nexus/helpers/extensions/show_user_popover.dart";
|
||||||
|
import "package:nexus/models/content/membership.dart";
|
||||||
|
import "package:nexus/models/room_summary.dart";
|
||||||
|
|
||||||
class MentionChip extends ConsumerWidget {
|
class MentionChip extends ConsumerWidget {
|
||||||
final String? roomId;
|
final String? roomId;
|
||||||
|
|
@ -12,29 +15,48 @@ class MentionChip extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final mention = content.mention;
|
final mention = content.mention;
|
||||||
final membership = mention?.startsWith("@") == true
|
final data = switch (mention?.characters.firstOrNull) {
|
||||||
? ref
|
"@" =>
|
||||||
.watch(
|
ref
|
||||||
UserController.provider(.new(roomId: roomId, userId: mention!)),
|
.watch(
|
||||||
)
|
UserController.provider(.new(roomId: roomId, userId: mention!)),
|
||||||
.whenOrNull(data: (data) => data)
|
)
|
||||||
: null;
|
.whenOrNull(data: (data) => data),
|
||||||
|
|
||||||
|
"#" || "!" =>
|
||||||
|
ref
|
||||||
|
.watch(
|
||||||
|
RoomSummaryController.provider(.new(roomIdOrAlias: mention!)),
|
||||||
|
)
|
||||||
|
.whenOrNull(data: (data) => data),
|
||||||
|
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
|
||||||
return mention == null
|
return mention == null
|
||||||
? SizedBox.shrink()
|
? SizedBox.shrink()
|
||||||
: InkWell(
|
: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
if (membership != null) {
|
if (data case MembershipContent membership) {
|
||||||
context.showUserPopover(membership, mention, roomId: roomId);
|
context.showUserPopover(membership, mention, roomId: roomId);
|
||||||
|
} else if (data case RoomSummary summary) {
|
||||||
|
// TODO: Handle summary
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
child: Chip(
|
child: Chip(
|
||||||
label: Text(
|
label: Text(
|
||||||
(membership?.displayName == null
|
switch (data) {
|
||||||
? null
|
RoomSummary summary =>
|
||||||
: "@${membership!.displayName}") ??
|
(summary.name == null ? null : "#${summary.name}") ??
|
||||||
mention,
|
summary.canonicalAlias ??
|
||||||
|
summary.roomId,
|
||||||
|
MembershipContent membership =>
|
||||||
|
membership.displayName == null
|
||||||
|
? mention
|
||||||
|
: "@${membership.displayName}",
|
||||||
|
_ => mention,
|
||||||
|
},
|
||||||
style: .new(
|
style: .new(
|
||||||
fontWeight: .bold,
|
fontWeight: .bold,
|
||||||
color: Theme.of(context).colorScheme.onPrimary,
|
color: Theme.of(context).colorScheme.onPrimary,
|
||||||
|
|
|
||||||
|
|
@ -72,40 +72,24 @@ class RoomChat extends HookConsumerWidget {
|
||||||
|
|
||||||
final controllerProvider = RoomChatController.provider(roomId);
|
final controllerProvider = RoomChatController.provider(roomId);
|
||||||
final notifier = ref.watch(controllerProvider.notifier);
|
final notifier = ref.watch(controllerProvider.notifier);
|
||||||
|
|
||||||
final client = ref.watch(ClientController.provider.notifier);
|
final client = ref.watch(ClientController.provider.notifier);
|
||||||
|
|
||||||
|
final listController = useRef(ListController());
|
||||||
final scrollController = useScrollController();
|
final scrollController = useScrollController();
|
||||||
final controllerData = ref.watch(controllerProvider);
|
final controllerData = ref.watch(controllerProvider);
|
||||||
final historyListController = useRef(ListController());
|
|
||||||
final liveListController = useRef(ListController());
|
final topEventBeforeLoad = useState<String?>(null);
|
||||||
final timelineCenterKey = useMemoized(GlobalKey.new);
|
final hasMore = useState<bool>(true);
|
||||||
final timelineBoundary = useRef<String?>(null);
|
|
||||||
final hasMore = ref.watch(
|
|
||||||
RoomsController.provider.select(
|
|
||||||
(rooms) => rooms[roomId]?.hasMore ?? false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> jumpToId(String eventId) async {
|
Future<void> jumpToId(String eventId) async {
|
||||||
final events = controllerData.value;
|
final index = controllerData.value?.indexWhere(
|
||||||
if (events == null || !scrollController.hasClients) return;
|
(element) => element.eventId == eventId,
|
||||||
|
|
||||||
final sourceIndex = events.indexWhere(
|
|
||||||
(event) => event.eventId == eventId,
|
|
||||||
);
|
);
|
||||||
final boundaryIndex = events.indexWhere(
|
if (index == null) return;
|
||||||
(event) => event.eventId == timelineBoundary.value,
|
|
||||||
);
|
|
||||||
if (sourceIndex < 0 || boundaryIndex < 0) return;
|
|
||||||
|
|
||||||
final listController = sourceIndex <= boundaryIndex
|
listController.value.animateToItem(
|
||||||
? historyListController.value
|
index: index,
|
||||||
: liveListController.value;
|
|
||||||
if (!listController.isAttached) return;
|
|
||||||
listController.animateToItem(
|
|
||||||
index: sourceIndex <= boundaryIndex
|
|
||||||
? boundaryIndex - sourceIndex
|
|
||||||
: sourceIndex - boundaryIndex - 1,
|
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
alignment: 0.5,
|
alignment: 0.5,
|
||||||
duration: (_) => .new(milliseconds: 700),
|
duration: (_) => .new(milliseconds: 700),
|
||||||
|
|
@ -119,6 +103,88 @@ class RoomChat extends HookConsumerWidget {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadOlder() async {
|
||||||
|
if (controllerData case AsyncData(:final value?)) {
|
||||||
|
topEventBeforeLoad.value = value.firstOrNull?.eventId;
|
||||||
|
hasMore.value = await notifier.loadOlder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
ref
|
||||||
|
.read(controllerProvider.future)
|
||||||
|
.then(
|
||||||
|
(_) => WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
scrollController.jumpTo(
|
||||||
|
scrollController.position.maxScrollExtent - .000001,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [scrollController.hasClients]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
if (controllerData case AsyncData(
|
||||||
|
:final value?,
|
||||||
|
) when scrollController.hasClients) {
|
||||||
|
if (topEventBeforeLoad.value != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
final index = value.indexWhere(
|
||||||
|
(event) => event.eventId == topEventBeforeLoad.value,
|
||||||
|
);
|
||||||
|
if (index != -1) {
|
||||||
|
listController.value.jumpToItem(
|
||||||
|
index: index,
|
||||||
|
scrollController: scrollController,
|
||||||
|
alignment: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
topEventBeforeLoad.value = null;
|
||||||
|
});
|
||||||
|
} else if (scrollController.position.atEdge &&
|
||||||
|
scrollController.position.pixels != 0) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
scrollController.jumpTo(
|
||||||
|
scrollController.position.maxScrollExtent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [controllerData]);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
Future<void> listener() async {
|
||||||
|
if (!scrollController.hasClients || !scrollController.position.atEdge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final room = ref.watch(
|
||||||
|
RoomsController.provider.select((value) => value[roomId]),
|
||||||
|
);
|
||||||
|
if (room == null) return;
|
||||||
|
|
||||||
|
if (scrollController.position.pixels == 0) {
|
||||||
|
if (room.hasMore) {
|
||||||
|
await loadOlder();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await client.markRead(room);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(listener);
|
||||||
|
return () => scrollController.removeListener(listener);
|
||||||
|
}, [roomId, controllerData]);
|
||||||
|
|
||||||
final composerNode = useFocusNode(
|
final composerNode = useFocusNode(
|
||||||
onKeyEvent: (_, event) {
|
onKeyEvent: (_, event) {
|
||||||
if (event is KeyDownEvent && event.logicalKey == .escape) {
|
if (event is KeyDownEvent && event.logicalKey == .escape) {
|
||||||
|
|
@ -245,7 +311,7 @@ class RoomChat extends HookConsumerWidget {
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(
|
||||||
ClipboardData(
|
ClipboardData(
|
||||||
text:
|
text:
|
||||||
"matrix:roomid/${room.metadata?.id.substring(1)}/e/${event.eventId}$vias)",
|
"matrix:roomid/${room.metadata?.id.substring(1)}/e/${event.eventId}$vias",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -400,120 +466,59 @@ class RoomChat extends HookConsumerWidget {
|
||||||
padding: .symmetric(horizontal: 4),
|
padding: .symmetric(horizontal: 4),
|
||||||
child: switch (controllerData) {
|
child: switch (controllerData) {
|
||||||
AsyncData(:final value?) ||
|
AsyncData(:final value?) ||
|
||||||
AsyncLoading(:final value?) => Builder(
|
AsyncLoading(:final value?) => CustomScrollView(
|
||||||
builder: (_) {
|
keyboardDismissBehavior:
|
||||||
if (value.isEmpty) return nothing;
|
ScrollViewKeyboardDismissBehavior.onDrag,
|
||||||
var boundaryIndex = value.indexWhere(
|
controller: scrollController,
|
||||||
(event) =>
|
slivers: [
|
||||||
event.eventId == timelineBoundary.value,
|
if (hasMore.value)
|
||||||
);
|
SliverToBoxAdapter(
|
||||||
if (boundaryIndex < 0) {
|
child: Padding(
|
||||||
boundaryIndex = value.length - 1;
|
padding: .symmetric(vertical: 36),
|
||||||
timelineBoundary.value =
|
child: Center(
|
||||||
value[boundaryIndex].eventId;
|
child: ElevatedButton(
|
||||||
}
|
onPressed: controllerData is AsyncData
|
||||||
|
? loadOlder
|
||||||
final historyLength = boundaryIndex + 1;
|
: null,
|
||||||
final liveLength = value.length - historyLength;
|
child: Text("Load More"),
|
||||||
|
|
||||||
int? eventIndexForKey(Key key) {
|
|
||||||
if (key is! ValueKey<String>) return null;
|
|
||||||
final index = value.indexWhere(
|
|
||||||
(event) => event.eventId == key.value,
|
|
||||||
);
|
|
||||||
return index < 0 ? null : index;
|
|
||||||
}
|
|
||||||
|
|
||||||
int? historyChildIndex(Key key) {
|
|
||||||
if (key == const ValueKey("history-loader")) {
|
|
||||||
return historyLength;
|
|
||||||
}
|
|
||||||
final eventIndex = eventIndexForKey(key);
|
|
||||||
return eventIndex == null ||
|
|
||||||
eventIndex >= historyLength
|
|
||||||
? null
|
|
||||||
: historyLength - eventIndex - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int? liveChildIndex(Key key) {
|
|
||||||
final eventIndex = eventIndexForKey(key);
|
|
||||||
return eventIndex == null ||
|
|
||||||
eventIndex < historyLength
|
|
||||||
? null
|
|
||||||
: eventIndex - historyLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget eventRow(
|
|
||||||
Event event,
|
|
||||||
Event? previousEvent,
|
|
||||||
) => HighlightWrapper(
|
|
||||||
EventRenderer(
|
|
||||||
event,
|
|
||||||
onTapReply: () => jumpToId(event.replyTo!),
|
|
||||||
getEventOptions: getEventOptions,
|
|
||||||
isGrouped:
|
|
||||||
previousEvent?.content
|
|
||||||
is MessageContent &&
|
|
||||||
previousEvent?.redactedBy == null &&
|
|
||||||
previousEvent?.relationType !=
|
|
||||||
"m.replace" &&
|
|
||||||
"${event.sender}${event.pmp?.id}" ==
|
|
||||||
"${previousEvent?.sender}${previousEvent?.pmp?.id}",
|
|
||||||
),
|
|
||||||
key: ValueKey(event.eventId),
|
|
||||||
isHighlighted:
|
|
||||||
highlightedEvent.value == event.eventId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return CustomScrollView(
|
|
||||||
reverse: true,
|
|
||||||
center: timelineCenterKey,
|
|
||||||
keyboardDismissBehavior:
|
|
||||||
ScrollViewKeyboardDismissBehavior.onDrag,
|
|
||||||
controller: scrollController,
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: SizedBox(height: composerSize.value),
|
|
||||||
),
|
|
||||||
SuperSliverList.builder(
|
|
||||||
listController: liveListController.value,
|
|
||||||
itemCount: liveLength,
|
|
||||||
findChildIndexCallback: liveChildIndex,
|
|
||||||
itemBuilder: (_, index) => eventRow(
|
|
||||||
value[historyLength + index],
|
|
||||||
value.getOrNull(
|
|
||||||
historyLength + index - 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SuperSliverList.builder(
|
),
|
||||||
key: timelineCenterKey,
|
|
||||||
listController: historyListController.value,
|
SuperSliverList.builder(
|
||||||
itemCount: historyLength + 1,
|
listController: listController.value,
|
||||||
findChildIndexCallback: historyChildIndex,
|
itemCount: value.length,
|
||||||
itemBuilder: (_, index) {
|
itemBuilder: (_, index) {
|
||||||
if (index == historyLength) {
|
final event = value[index];
|
||||||
return _HistoryLoader(
|
final previousEvent = value.getOrNull(
|
||||||
key: const ValueKey("history-loader"),
|
index - 1,
|
||||||
hasMore: hasMore,
|
);
|
||||||
isLoading:
|
return HighlightWrapper(
|
||||||
controllerData is AsyncLoading,
|
EventRenderer(
|
||||||
onLoad: notifier.loadOlder,
|
event,
|
||||||
);
|
onTapReply: () =>
|
||||||
}
|
jumpToId(event.replyTo!),
|
||||||
final event =
|
getEventOptions: getEventOptions,
|
||||||
value[historyLength - index - 1];
|
isGrouped:
|
||||||
return eventRow(
|
previousEvent?.content
|
||||||
event,
|
is MessageContent &&
|
||||||
value.getOrNull(
|
previousEvent?.redactedBy == null &&
|
||||||
historyLength - index - 2,
|
previousEvent?.relationType !=
|
||||||
),
|
"m.replace" &&
|
||||||
);
|
"${event.sender}${event.pmp?.id}" ==
|
||||||
},
|
"${previousEvent?.sender}${previousEvent?.pmp?.id}",
|
||||||
),
|
),
|
||||||
],
|
isHighlighted:
|
||||||
);
|
highlightedEvent.value == event.eventId,
|
||||||
},
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
SliverPadding(
|
||||||
|
padding: .only(bottom: composerSize.value),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
AsyncData() => nothing,
|
AsyncData() => nothing,
|
||||||
AsyncLoading() => Loading(),
|
AsyncLoading() => Loading(),
|
||||||
|
|
@ -561,42 +566,3 @@ class RoomChat extends HookConsumerWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryLoader extends HookWidget {
|
|
||||||
final bool hasMore;
|
|
||||||
final bool isLoading;
|
|
||||||
final Future<void> Function() onLoad;
|
|
||||||
|
|
||||||
const _HistoryLoader({
|
|
||||||
required this.hasMore,
|
|
||||||
required this.isLoading,
|
|
||||||
required this.onLoad,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
useEffect(() {
|
|
||||||
if (hasMore) {
|
|
||||||
Future.microtask(onLoad);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}, const []);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: .symmetric(vertical: 36),
|
|
||||||
child: Center(
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: hasMore && !isLoading ? onLoad : null,
|
|
||||||
child: isLoading
|
|
||||||
? SizedBox(
|
|
||||||
height: 20,
|
|
||||||
width: 20,
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: Text(hasMore ? "Load More" : "Start of timeline"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,7 @@ class RoomMenu extends ConsumerWidget {
|
||||||
|
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(
|
||||||
.new(
|
.new(
|
||||||
text:
|
text: "matrix:roomid/${room!.metadata?.id.substring(1)}$vias",
|
||||||
"matrix:roomid/${room!.metadata?.id.substring(1)}$vias)",
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
platform :osx, '10.15'
|
platform :osx, '13.0'
|
||||||
|
|
||||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
|
|
|
||||||
|
|
@ -567,7 +567,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|
@ -649,7 +649,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = YES;
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
|
|
@ -699,7 +699,7 @@
|
||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
MACOSX_DEPLOYMENT_TARGET = 10.15;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,68 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"info": {
|
||||||
{
|
"version": 1,
|
||||||
"size" : "16x16",
|
"author": "xcode"
|
||||||
"idiom" : "mac",
|
|
||||||
"filename" : "app_icon_16.png",
|
|
||||||
"scale" : "1x"
|
|
||||||
},
|
},
|
||||||
{
|
"images": [
|
||||||
"size" : "16x16",
|
{
|
||||||
"idiom" : "mac",
|
"size": "16x16",
|
||||||
"filename" : "app_icon_32.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_16.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "32x32",
|
{
|
||||||
"idiom" : "mac",
|
"size": "16x16",
|
||||||
"filename" : "app_icon_32.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_32.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "32x32",
|
{
|
||||||
"idiom" : "mac",
|
"size": "32x32",
|
||||||
"filename" : "app_icon_64.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_32.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "128x128",
|
{
|
||||||
"idiom" : "mac",
|
"size": "32x32",
|
||||||
"filename" : "app_icon_128.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_64.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "128x128",
|
{
|
||||||
"idiom" : "mac",
|
"size": "128x128",
|
||||||
"filename" : "app_icon_256.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_128.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "256x256",
|
{
|
||||||
"idiom" : "mac",
|
"size": "128x128",
|
||||||
"filename" : "app_icon_256.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_256.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "256x256",
|
{
|
||||||
"idiom" : "mac",
|
"size": "256x256",
|
||||||
"filename" : "app_icon_512.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_256.png",
|
||||||
},
|
"scale": "1x"
|
||||||
{
|
},
|
||||||
"size" : "512x512",
|
{
|
||||||
"idiom" : "mac",
|
"size": "256x256",
|
||||||
"filename" : "app_icon_512.png",
|
"idiom": "mac",
|
||||||
"scale" : "1x"
|
"filename": "app_icon_512.png",
|
||||||
},
|
"scale": "2x"
|
||||||
{
|
},
|
||||||
"size" : "512x512",
|
{
|
||||||
"idiom" : "mac",
|
"size": "512x512",
|
||||||
"filename" : "app_icon_1024.png",
|
"idiom": "mac",
|
||||||
"scale" : "2x"
|
"filename": "app_icon_512.png",
|
||||||
}
|
"scale": "1x"
|
||||||
],
|
},
|
||||||
"info" : {
|
{
|
||||||
"version" : 1,
|
"size": "512x512",
|
||||||
"author" : "xcode"
|
"idiom": "mac",
|
||||||
}
|
"filename": "app_icon_1024.png",
|
||||||
}
|
"scale": "2x"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 520 B After Width: | Height: | Size: 678 B |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.7 KiB |
|
|
@ -87,12 +87,15 @@ dev_dependencies:
|
||||||
json_serializable: 6.14.1
|
json_serializable: 6.14.1
|
||||||
|
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons:
|
||||||
ios: true
|
|
||||||
android: true
|
|
||||||
image_path: assets/icon.png
|
image_path: assets/icon.png
|
||||||
|
ios: true
|
||||||
|
image_path_ios: assets/mobile.png
|
||||||
|
android: true
|
||||||
adaptive_icon_background: assets/background.png
|
adaptive_icon_background: assets/background.png
|
||||||
adaptive_icon_foreground: assets/foreground.png
|
adaptive_icon_foreground: assets/foreground.png
|
||||||
adaptive_icon_monochrome: assets/monochrome.png
|
adaptive_icon_monochrome: assets/monochrome.png
|
||||||
remove_alpha_ios: true
|
|
||||||
windows:
|
windows:
|
||||||
generate: true
|
generate: true
|
||||||
|
macos:
|
||||||
|
generate: true
|
||||||
|
image_path: assets/mobile.png
|
||||||
|
|
|
||||||