diff --git a/.gitignore b/.gitignore index 33ae96a89..4ba6b0062 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ app.*.map.json # builder **/node_modules/ +**/build/ # Release /private_keys/ \ No newline at end of file diff --git a/lib/model/tab_setting.dart b/lib/model/tab_setting.dart index 91df3c860..ece13aed3 100644 --- a/lib/model/tab_setting.dart +++ b/lib/model/tab_setting.dart @@ -22,6 +22,9 @@ class TabSetting with _$TabSetting { /// タブ種別 required TabType tabType, + /// ロールタイムラインのノートの場合、ロールID + String? roleId, + /// チャンネルのノートの場合、チャンネルID String? channelId, diff --git a/lib/model/tab_setting.freezed.dart b/lib/model/tab_setting.freezed.dart index 40f6fa171..218a40d51 100644 --- a/lib/model/tab_setting.freezed.dart +++ b/lib/model/tab_setting.freezed.dart @@ -26,6 +26,9 @@ mixin _$TabSetting { /// タブ種別 TabType get tabType => throw _privateConstructorUsedError; + /// ロールタイムラインのノートの場合、ロールID + String? get roleId => throw _privateConstructorUsedError; + /// チャンネルのノートの場合、チャンネルID String? get channelId => throw _privateConstructorUsedError; @@ -60,6 +63,7 @@ abstract class $TabSettingCopyWith<$Res> { $Res call( {@IconDataConverter() TabIcon icon, TabType tabType, + String? roleId, String? channelId, String? listId, String? antennaId, @@ -87,6 +91,7 @@ class _$TabSettingCopyWithImpl<$Res, $Val extends TabSetting> $Res call({ Object? icon = null, Object? tabType = null, + Object? roleId = freezed, Object? channelId = freezed, Object? listId = freezed, Object? antennaId = freezed, @@ -104,6 +109,10 @@ class _$TabSettingCopyWithImpl<$Res, $Val extends TabSetting> ? _value.tabType : tabType // ignore: cast_nullable_to_non_nullable as TabType, + roleId: freezed == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as String?, channelId: freezed == channelId ? _value.channelId : channelId // ignore: cast_nullable_to_non_nullable @@ -163,6 +172,7 @@ abstract class _$$_TabSettingCopyWith<$Res> $Res call( {@IconDataConverter() TabIcon icon, TabType tabType, + String? roleId, String? channelId, String? listId, String? antennaId, @@ -190,6 +200,7 @@ class __$$_TabSettingCopyWithImpl<$Res> $Res call({ Object? icon = null, Object? tabType = null, + Object? roleId = freezed, Object? channelId = freezed, Object? listId = freezed, Object? antennaId = freezed, @@ -207,6 +218,10 @@ class __$$_TabSettingCopyWithImpl<$Res> ? _value.tabType : tabType // ignore: cast_nullable_to_non_nullable as TabType, + roleId: freezed == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as String?, channelId: freezed == channelId ? _value.channelId : channelId // ignore: cast_nullable_to_non_nullable @@ -242,6 +257,7 @@ class _$_TabSetting extends _TabSetting { const _$_TabSetting( {@IconDataConverter() required this.icon, required this.tabType, + this.roleId, this.channelId, this.listId, this.antennaId, @@ -262,6 +278,10 @@ class _$_TabSetting extends _TabSetting { @override final TabType tabType; + /// ロールタイムラインのノートの場合、ロールID + @override + final String? roleId; + /// チャンネルのノートの場合、チャンネルID @override final String? channelId; @@ -292,7 +312,7 @@ class _$_TabSetting extends _TabSetting { @override String toString() { - return 'TabSetting(icon: $icon, tabType: $tabType, channelId: $channelId, listId: $listId, antennaId: $antennaId, isSubscribe: $isSubscribe, name: $name, account: $account, renoteDisplay: $renoteDisplay)'; + return 'TabSetting(icon: $icon, tabType: $tabType, roleId: $roleId, channelId: $channelId, listId: $listId, antennaId: $antennaId, isSubscribe: $isSubscribe, name: $name, account: $account, renoteDisplay: $renoteDisplay)'; } @override @@ -302,6 +322,7 @@ class _$_TabSetting extends _TabSetting { other is _$_TabSetting && (identical(other.icon, icon) || other.icon == icon) && (identical(other.tabType, tabType) || other.tabType == tabType) && + (identical(other.roleId, roleId) || other.roleId == roleId) && (identical(other.channelId, channelId) || other.channelId == channelId) && (identical(other.listId, listId) || other.listId == listId) && @@ -321,6 +342,7 @@ class _$_TabSetting extends _TabSetting { runtimeType, icon, tabType, + roleId, channelId, listId, antennaId, @@ -347,6 +369,7 @@ abstract class _TabSetting extends TabSetting { const factory _TabSetting( {@IconDataConverter() required final TabIcon icon, required final TabType tabType, + final String? roleId, final String? channelId, final String? listId, final String? antennaId, @@ -368,6 +391,10 @@ abstract class _TabSetting extends TabSetting { TabType get tabType; @override + /// ロールタイムラインのノートの場合、ロールID + String? get roleId; + @override + /// チャンネルのノートの場合、チャンネルID String? get channelId; @override diff --git a/lib/model/tab_setting.g.dart b/lib/model/tab_setting.g.dart index 6d467e2c7..64258833f 100644 --- a/lib/model/tab_setting.g.dart +++ b/lib/model/tab_setting.g.dart @@ -10,6 +10,7 @@ _$_TabSetting _$$_TabSettingFromJson(Map json) => _$_TabSetting( icon: const IconDataConverter().fromJson(json['icon']), tabType: $enumDecode(_$TabTypeEnumMap, json['tabType']), + roleId: json['roleId'] as String?, channelId: json['channelId'] as String?, listId: json['listId'] as String?, antennaId: json['antennaId'] as String?, @@ -23,6 +24,7 @@ Map _$$_TabSettingToJson(_$_TabSetting instance) => { 'icon': const IconDataConverter().toJson(instance.icon), 'tabType': _$TabTypeEnumMap[instance.tabType]!, + 'roleId': instance.roleId, 'channelId': instance.channelId, 'listId': instance.listId, 'antennaId': instance.antennaId, @@ -37,6 +39,7 @@ const _$TabTypeEnumMap = { TabType.homeTimeline: 'homeTimeline', TabType.globalTimeline: 'globalTimeline', TabType.hybridTimeline: 'hybridTimeline', + TabType.roleTimeline: 'roleTimeline', TabType.channel: 'channel', TabType.userList: 'userList', TabType.antenna: 'antenna', diff --git a/lib/model/tab_type.dart b/lib/model/tab_type.dart index 970cbf901..ea0fe45d8 100644 --- a/lib/model/tab_type.dart +++ b/lib/model/tab_type.dart @@ -8,6 +8,7 @@ enum TabType { homeTimeline("ホームタイムライン"), globalTimeline("グローバルタイムライン"), hybridTimeline("ソーシャルタイムライン"), + roleTimeline("ロールタイムライン"), channel("チャンネル"), userList("リスト"), antenna("アンテナ"), @@ -27,6 +28,8 @@ enum TabType { return globalTimeLineProvider(setting); case TabType.hybridTimeline: return hybridTimeLineProvider(setting); //FIXME + case TabType.roleTimeline: + return roleTimelineProvider(setting); case TabType.channel: return channelTimelineProvider(setting); case TabType.userList: diff --git a/lib/providers.dart b/lib/providers.dart index 569a42da2..1286d7de9 100644 --- a/lib/providers.dart +++ b/lib/providers.dart @@ -17,6 +17,7 @@ import 'package:miria/repository/main_stream_repository.dart'; import 'package:miria/repository/global_time_line_repository.dart'; import 'package:miria/repository/home_time_line_repository.dart'; import 'package:miria/repository/local_time_line_repository.dart'; +import 'package:miria/repository/role_timeline_repository.dart'; import 'package:miria/repository/note_repository.dart'; import 'package:miria/repository/tab_settings_repository.dart'; import 'package:miria/repository/time_line_repository.dart'; @@ -85,6 +86,19 @@ final hybridTimeLineProvider = ref.read(emojiRepositoryProvider(tabSetting.account)), )); +final roleTimelineProvider = + ChangeNotifierProvider.family( + (ref, tabSetting) => RoleTimelineRepository( + ref.read(misskeyProvider(tabSetting.account)), + ref.read(notesProvider(tabSetting.account)), + ref.read(mainStreamRepositoryProvider(tabSetting.account)), + ref.read(generalSettingsRepositoryProvider), + tabSetting, + ref.read(mainStreamRepositoryProvider(tabSetting.account)), + ref.read(accountRepository), + ref.read(emojiRepositoryProvider(tabSetting.account)), + )); + final channelTimelineProvider = ChangeNotifierProvider.family( (ref, tabSetting) => ChannelTimelineRepository( diff --git a/lib/repository/account_repository.dart b/lib/repository/account_repository.dart index a71a3bf73..06a56d9e0 100644 --- a/lib/repository/account_repository.dart +++ b/lib/repository/account_repository.dart @@ -88,6 +88,7 @@ class AccountRepository extends ChangeNotifier { notifyListeners(); } + //一つ目のアカウントが追加されたときに自動で追加されるタブ Future _addIfTabSettingNothing() async { if (_account.length == 1) { final account = _account.first; diff --git a/lib/repository/role_timeline_repository.dart b/lib/repository/role_timeline_repository.dart new file mode 100644 index 000000000..296476407 --- /dev/null +++ b/lib/repository/role_timeline_repository.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:miria/repository/socket_timeline_repository.dart'; +import 'package:misskey_dart/misskey_dart.dart'; + +class RoleTimelineRepository extends SocketTimelineRepository { + RoleTimelineRepository( + super.misskey, + super.noteRepository, + super.globalNotificationRepository, + super.generalSettingsRepository, + super.tabSetting, + super.mainStreamRepository, + super.accountRepository, + super.emojiRepository, + ); + + @override + SocketController createSocketController({ + required void Function(Note note) onReceived, + required FutureOr Function(String id, TimelineReacted reaction) + onReacted, + required FutureOr Function(String id, TimelineVoted vote) onVoted, + }) { + return misskey.roleTimelineStream( + roleId: tabSetting.roleId!, + onNoteReceived: onReceived, + onReacted: onReacted, + onVoted: onVoted, + ); + } + + @override + Future> requestNotes({String? untilId}) async { + return await misskey.roles.notes(RolesNotesRequest( + roleId: tabSetting.roleId!, + limit: 30, + untilId: untilId, + )); + } +} diff --git a/lib/view/settings_page/tab_settings_page/icon_select_dialog.dart b/lib/view/settings_page/tab_settings_page/icon_select_dialog.dart index 3903690a5..ac0c264b7 100644 --- a/lib/view/settings_page/tab_settings_page/icon_select_dialog.dart +++ b/lib/view/settings_page/tab_settings_page/icon_select_dialog.dart @@ -18,6 +18,8 @@ class IconSelectDialog extends StatelessWidget { Icons.rocket_outlined, Icons.rocket_launch, Icons.rocket_launch_outlined, + Icons.bookmark, + Icons.bookmark_outline, Icons.hub, Icons.hub_outlined, Icons.settings_input_antenna, diff --git a/lib/view/settings_page/tab_settings_page/role_select_dialog.dart b/lib/view/settings_page/tab_settings_page/role_select_dialog.dart new file mode 100644 index 000000000..5c51360d0 --- /dev/null +++ b/lib/view/settings_page/tab_settings_page/role_select_dialog.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:miria/model/account.dart'; +import 'package:miria/providers.dart'; +import 'package:miria/view/common/account_scope.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:misskey_dart/misskey_dart.dart'; + +class RoleSelectDialog extends ConsumerStatefulWidget { + final Account account; + + const RoleSelectDialog({super.key, required this.account}); + + @override + ConsumerState createState() => + RoleSelectDialogState(); +} + +class RoleSelectDialogState extends ConsumerState { + final roles = []; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + Future(() async { + final rolesList = await ref + .read(misskeyProvider(widget.account)) + .roles + .list(); + roles + ..clear() + ..addAll(rolesList); + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return AccountScope( + account: widget.account, + child: AlertDialog( + title: const Text("ロール選択"), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "ロール", + style: Theme.of(context).textTheme.titleMedium, + ), + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: roles.length, + itemBuilder: (context, index) { + return ListTile( + onTap: () { + Navigator.of(context).pop(roles[index]); + }, + title: Text(roles[index].name)); + }), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/settings_page/tab_settings_page/tab_settings_page.dart b/lib/view/settings_page/tab_settings_page/tab_settings_page.dart index c179a7ea4..4fdeb695e 100644 --- a/lib/view/settings_page/tab_settings_page/tab_settings_page.dart +++ b/lib/view/settings_page/tab_settings_page/tab_settings_page.dart @@ -8,6 +8,7 @@ import 'package:miria/providers.dart'; import 'package:miria/view/common/account_scope.dart'; import 'package:miria/view/dialogs/simple_message_dialog.dart'; import 'package:miria/view/common/tab_icon_view.dart'; +import 'package:miria/view/settings_page/tab_settings_page/role_select_dialog.dart'; import 'package:miria/view/settings_page/tab_settings_page/antenna_select_dialog.dart'; import 'package:miria/view/settings_page/tab_settings_page/channel_select_dialog.dart'; import 'package:miria/view/settings_page/tab_settings_page/icon_select_dialog.dart'; @@ -29,6 +30,7 @@ class TabSettingsPage extends ConsumerStatefulWidget { class TabSettingsAddDialogState extends ConsumerState { late Account? selectedAccount = ref.read(accountRepository).account.first; TabType? selectedTabType = TabType.localTimeline; + RolesListResponse? selectedRole; CommunityChannel? selectedChannel; UsersList? selectedUserList; Antenna? selectedAntenna; @@ -47,6 +49,7 @@ class TabSettingsAddDialogState extends ConsumerState { ref.read(tabSettingsRepositoryProvider).tabSettings.toList()[tab]; selectedAccount = tabSetting.account; selectedTabType = tabSetting.tabType; + final roleId = tabSetting.roleId; final channelId = tabSetting.channelId; final listId = tabSetting.listId; final antennaId = tabSetting.antennaId; @@ -54,6 +57,15 @@ class TabSettingsAddDialogState extends ConsumerState { selectedIcon = tabSetting.icon; renoteDisplay = tabSetting.renoteDisplay; isSubscribe = tabSetting.isSubscribe; + if (roleId != null) { + Future(() async { + selectedRole = await ref + .read(misskeyProvider(tabSetting.account)) + .roles + .show(RolesShowRequest(roleId: roleId)); + setState(() {}); + }); + } if (channelId != null) { Future(() async { selectedChannel = await ref @@ -160,6 +172,29 @@ class TabSettingsAddDialogState extends ConsumerState { value: selectedTabType, ), const Padding(padding: EdgeInsets.all(10)), + if (selectedTabType == TabType.roleTimeline) ...[ + const Text("ロールタイムライン"), + Row( + children: [ + Expanded(child: Text(selectedRole?.name ?? "")), + IconButton( + onPressed: () async { + final selected = selectedAccount; + if (selected == null) return; + + selectedRole = await showDialog( + context: context, + builder: (context) => + RoleSelectDialog(account: selected)); + setState(() { + nameController.text = + selectedRole?.name ?? nameController.text; + }); + }, + icon: const Icon(Icons.navigate_next)) + ], + ) + ], if (selectedTabType == TabType.channel) ...[ const Text("チャンネル"), Row( @@ -323,6 +358,7 @@ class TabSettingsAddDialogState extends ConsumerState { tabType: tabType, name: nameController.text, account: account, + roleId: selectedRole?.id, channelId: selectedChannel?.id, listId: selectedUserList?.id, antennaId: selectedAntenna?.id,