From d697bc7b8a3f06cf3dfe7faf6a4d3ee1e85f00e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E7=A5=A5=E4=BF=8A?= Date: Thu, 26 Oct 2023 14:06:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B8=B8=E6=88=8F=E5=BC=80=E6=9C=8D/?= =?UTF-8?q?=E5=BC=80=E6=B5=8B=E6=B6=88=E6=81=AF=E9=80=9A=E7=9F=A5=E2=80=94?= =?UTF-8?q?=E5=AE=A2=E6=88=B7=E7=AB=AF=20https://jira.shanqu.cc/browse/GHZ?= =?UTF-8?q?S-3350?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/proguard-rules.txt | 4 + app/src/main/AndroidManifest.xml | 8 + .../java/com/gh/common/util/DirectUtils.kt | 27 +- .../java/com/gh/common/util/NewLogUtils.kt | 205 +++++++++++ .../java/com/gh/gamecenter/SkipActivity.java | 2 +- .../gh/gamecenter/entity/CalendarEntity.kt | 15 +- .../entity/ServerSubscriptionEntity.kt | 10 + .../fragment/SearchToolbarFragment.java | 3 +- .../gamedetail/GameDetailFragment.kt | 2 +- .../fuli/kaifu/ServersCalendarActivity.kt | 299 ++++++++-------- .../fuli/kaifu/ServersCalendarAdapter.kt | 2 + .../fuli/kaifu/ServersCalendarAdvancedTime.kt | 18 + .../kaifu/ServersCalendarDetailAdapter.kt | 151 +++++++++ .../fuli/kaifu/ServersCalendarDetailDialog.kt | 164 +++++++++ .../ServersCalendarDetailNoDataDialog.kt | 281 +++++++++++++++ .../ServersCalendarDetailNoDataViewModel.kt | 140 ++++++++ ... => ServersCalendarDetailReportAdapter.kt} | 29 +- .../kaifu/ServersCalendarDetailViewModel.kt | 38 +++ .../ServersCalendarDetailsRemindDialog.kt | 320 ++++++++++++++++++ .../ServersCalendarDetailsRemindViewModel.kt | 160 +++++++++ .../fuli/kaifu/ServersCalendarListAdapter.kt | 176 ++++++++++ .../fuli/kaifu/ServersCalendarListFragment.kt | 143 ++++++++ .../kaifu/ServersCalendarListViewModel.kt | 77 +++++ .../ServersCalendarManagementActivity.kt | 40 +++ .../fuli/kaifu/ServersCalendarMoreDialog.kt | 63 ++++ .../kaifu/ServersCalendarRemindListAdapter.kt | 186 ++++++++++ .../ServersCalendarRemindListFragment.kt | 37 ++ .../ServersCalendarRemindListViewModel.kt | 44 +++ ...ServersCalendarRemindTimeSettingAdapter.kt | 32 ++ .../ServersCalendarRemindTimeSettingDialog.kt | 59 ++++ ...erversCalendarSubscriptionSuccessDialog.kt | 43 +++ .../kaifu/ServersCalendarTimeSettingDialog.kt | 229 +++++++++++++ .../fuli/kaifu/ServersCalendarViewModel.kt | 184 ++++++++-- ...CalendarWeChatSubscriptionSuccessDialog.kt | 58 ++++ .../kaifu/ServersDetailReportViewHolder.kt | 28 -- .../fuli/kaifu/ServersDetailViewHolder.kt | 47 --- .../ServersSubscribedGameListActivity.kt | 36 ++ .../kaifu/ServersSubscribedGameListAdapter.kt | 64 ++++ .../ServersSubscribedGameListFragment.kt | 80 +++++ .../ServersSubscribedGameListViewModel.kt | 62 ++++ .../message/MessageUnreadRepository.kt | 28 +- .../personal/HaloPersonalBannerAdapter.kt | 4 + .../personal/HaloPersonalFragment.kt | 1 + .../personal/HaloPersonalFunctionAdapter.kt | 4 + .../personal/HaloPersonalRecommendAdapter.kt | 4 + .../personal/HaloPersonalViewModel.kt | 5 +- .../detail/ArticleDetailRecyclerView.kt | 22 +- .../video/detail/ForumVideoDetailFragment.kt | 2 +- .../gamecenter/retrofit/RetrofitManager.java | 5 +- .../retrofit/service/ApiService.java | 98 +++++- ...u_gamedetail_more.xml => ic_menu_more.xml} | 0 .../bg_servers_calendar_reserve_success.webp | Bin 0 -> 73386 bytes ..._servers_calendar_detail_cancel_remind.png | Bin 0 -> 1050 bytes .../ic_servers_calendar_detail_remind.png | Bin 0 -> 1075 bytes .../ic_servers_subscribed_game_management.png | Bin 0 -> 652 bytes .../bg_datetime_picker_mask_bottom.xml | 8 + .../drawable/bg_datetime_picker_mask_top.xml | 8 + .../drawable/bg_server_detail_remind_time.xml | 10 + .../bg_server_detail_remind_time_disabled.xml | 10 + .../bg_servers_detail_cancel_no_data.xml | 8 + .../bg_servers_detail_cancel_remind.xml | 8 + .../res/drawable/bg_shape_f5_radius_2.xml | 6 + .../bg_shape_white_radius_12_top_only.xml | 10 + ...u_gamedetail_more.xml => ic_menu_more.xml} | 0 ..._more_light.xml => ic_menu_more_light.xml} | 0 ...or_servers_calendar_detail_item_remind.xml | 6 + .../res/layout/activity_servers_calendar.xml | 40 +++ .../layout/dialog_servers_calendar_more.xml | 77 +++++ ...g_servers_calendar_remind_time_setting.xml | 54 +++ .../dialog_servers_calendar_time_setting.xml | 105 ++++++ .../dialog_servers_calendear_detail.xml | 36 +- .../dialog_servers_calendear_detail_item.xml | 34 +- ...dialog_servers_calendear_detail_report.xml | 7 +- ...g_servers_calendear_detail_report_item.xml | 4 +- .../layout/dialog_servers_detail_remind.xml | 170 ++++++++++ .../fragment_game_server_test_v2_list.xml | 7 +- .../layout/fragment_servers_calendar_list.xml | 95 ++++++ .../layout/item_server_calendar_server.xml | 28 ++ .../main/res/layout/item_servers_calendar.xml | 84 +++++ .../layout/item_servers_calendar_remind.xml | 87 +++++ ...m_servers_calendar_remind_time_setting.xml | 22 ++ .../layout/item_servers_subscribed_game.xml | 71 ++++ .../layout_servers_calendar_detail_list.xml | 25 ++ ...layout_servers_calendar_detail_no_data.xml | 114 +++++++ app/src/main/res/menu/menu_article_detail.xml | 2 +- app/src/main/res/menu/menu_comment_detail.xml | 2 +- .../main/res/menu/menu_forum_video_detail.xml | 2 +- app/src/main/res/menu/menu_game_detail.xml | 2 +- .../res/menu/menu_server_calendar_more.xml | 12 + app/src/main/res/values/strings.xml | 46 +++ module_common/proguard-rules.pro | 5 +- .../gamecenter/common/constant/Constants.java | 4 + .../common/constant/EntranceConsts.java | 7 + .../livedata/NonStickyMutableLiveData.kt | 54 +++ .../gamecenter/common/utils/SensorsBridge.kt | 210 ++++++++++++ .../common/utils/WXAPIProxyFactory.java | 136 ++++++++ .../res/drawable/bg_shape_f8_radius_8.xml | 0 .../src/main/res/values-night/colors.xml | 4 + module_common/src/main/res/values/colors.xml | 5 + .../feature/entity/MessageDigestEntity.kt | 69 ++++ .../feature/entity/ServerCalendarEntity.kt | 9 +- .../entity/ServerCalendarFormEntity.kt | 9 + .../feature/entity/ServerCalendarGame.kt | 52 +++ .../entity/ServerCalendarNotifySetting.kt | 25 ++ .../feature/entity/WXSubscribeMsgConfig.kt | 13 + .../gh/gamecenter/wxapi/WXEntryActivity.java | 3 + .../message/entity/MessageGameEntity.kt | 5 +- .../gamecenter/message/utils/NewLogUtils.kt | 6 + .../message/view/KeFuFragmentAdapter.java | 8 +- .../message/view/MessageFragment.kt | 3 +- .../message/view/MessageNormalFragment.java | 2 +- .../view/message/MessageItemViewHolder.java | 167 ++++++++- .../view/message/MessageListActivity.kt | 3 +- .../view/message/MessageListAdapter.kt | 102 +++++- .../view/message/MessageListFragment.kt | 4 +- .../view/message/SortedMessageListAdapter.kt | 6 + .../src/main/res/layout/message_kefu_item.xml | 15 + .../src/main/res/layout/message_line_item.xml | 48 +++ .../gamecenter/sensorsdata/SensorsHelper.kt | 2 +- 119 files changed, 5587 insertions(+), 328 deletions(-) create mode 100644 app/src/main/java/com/gh/gamecenter/entity/ServerSubscriptionEntity.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarAdvancedTime.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailAdapter.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailDialog.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailNoDataDialog.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailNoDataViewModel.kt rename app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/{ServersDetailReportAdapter.kt => ServersCalendarDetailReportAdapter.kt} (57%) create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailViewModel.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailsRemindDialog.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailsRemindViewModel.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListAdapter.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListFragment.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListViewModel.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarManagementActivity.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarMoreDialog.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListAdapter.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListFragment.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListViewModel.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindTimeSettingAdapter.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindTimeSettingDialog.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarSubscriptionSuccessDialog.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarTimeSettingDialog.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarWeChatSubscriptionSuccessDialog.kt delete mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailReportViewHolder.kt delete mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailViewHolder.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListActivity.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListAdapter.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListFragment.kt create mode 100644 app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListViewModel.kt rename app/src/main/res/drawable-night/{ic_menu_gamedetail_more.xml => ic_menu_more.xml} (100%) create mode 100644 app/src/main/res/drawable-xxxhdpi/bg_servers_calendar_reserve_success.webp create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_servers_calendar_detail_cancel_remind.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_servers_calendar_detail_remind.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_servers_subscribed_game_management.png create mode 100644 app/src/main/res/drawable/bg_datetime_picker_mask_bottom.xml create mode 100644 app/src/main/res/drawable/bg_datetime_picker_mask_top.xml create mode 100644 app/src/main/res/drawable/bg_server_detail_remind_time.xml create mode 100644 app/src/main/res/drawable/bg_server_detail_remind_time_disabled.xml create mode 100644 app/src/main/res/drawable/bg_servers_detail_cancel_no_data.xml create mode 100644 app/src/main/res/drawable/bg_servers_detail_cancel_remind.xml create mode 100644 app/src/main/res/drawable/bg_shape_f5_radius_2.xml create mode 100644 app/src/main/res/drawable/bg_shape_white_radius_12_top_only.xml rename app/src/main/res/drawable/{ic_menu_gamedetail_more.xml => ic_menu_more.xml} (100%) rename app/src/main/res/drawable/{ic_menu_gamedetail_more_light.xml => ic_menu_more_light.xml} (100%) create mode 100644 app/src/main/res/drawable/selector_servers_calendar_detail_item_remind.xml create mode 100644 app/src/main/res/layout/dialog_servers_calendar_more.xml create mode 100644 app/src/main/res/layout/dialog_servers_calendar_remind_time_setting.xml create mode 100644 app/src/main/res/layout/dialog_servers_calendar_time_setting.xml create mode 100644 app/src/main/res/layout/dialog_servers_detail_remind.xml create mode 100644 app/src/main/res/layout/fragment_servers_calendar_list.xml create mode 100644 app/src/main/res/layout/item_server_calendar_server.xml create mode 100644 app/src/main/res/layout/item_servers_calendar.xml create mode 100644 app/src/main/res/layout/item_servers_calendar_remind.xml create mode 100644 app/src/main/res/layout/item_servers_calendar_remind_time_setting.xml create mode 100644 app/src/main/res/layout/item_servers_subscribed_game.xml create mode 100644 app/src/main/res/layout/layout_servers_calendar_detail_list.xml create mode 100644 app/src/main/res/layout/layout_servers_calendar_detail_no_data.xml create mode 100644 app/src/main/res/menu/menu_server_calendar_more.xml create mode 100644 module_common/src/main/java/com/gh/gamecenter/common/livedata/NonStickyMutableLiveData.kt create mode 100644 module_common/src/main/java/com/gh/gamecenter/common/utils/WXAPIProxyFactory.java rename {app => module_common}/src/main/res/drawable/bg_shape_f8_radius_8.xml (100%) create mode 100644 module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MessageDigestEntity.kt create mode 100644 module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarFormEntity.kt create mode 100644 module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarGame.kt create mode 100644 module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarNotifySetting.kt create mode 100644 module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/WXSubscribeMsgConfig.kt create mode 100644 module_message/src/main/res/layout/message_line_item.xml diff --git a/app/proguard-rules.txt b/app/proguard-rules.txt index 801a46e8bb..2142fe8df9 100644 --- a/app/proguard-rules.txt +++ b/app/proguard-rules.txt @@ -92,3 +92,7 @@ native ; } +-keepclassmembernames class com.contrarywind.view.WheelView { + private int itemsVisible; +} + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e1a8e04023..7291d315eb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -474,6 +474,14 @@ android:name="com.gh.gamecenter.gamedetail.fuli.kaifu.ServersCalendarActivity" android:screenOrientation="portrait" /> + + + + diff --git a/app/src/main/java/com/gh/common/util/DirectUtils.kt b/app/src/main/java/com/gh/common/util/DirectUtils.kt index 2f30221b19..c99818755b 100644 --- a/app/src/main/java/com/gh/common/util/DirectUtils.kt +++ b/app/src/main/java/com/gh/common/util/DirectUtils.kt @@ -63,6 +63,8 @@ import com.gh.gamecenter.gamecollection.hotlist.GameCollectionListDetailActivity import com.gh.gamecenter.gamecollection.square.GameCollectionSquareActivity import com.gh.gamecenter.gamedetail.GameDetailFragment import com.gh.gamecenter.gamedetail.fuli.kaifu.ServersCalendarActivity +import com.gh.gamecenter.gamedetail.fuli.kaifu.ServersCalendarManagementActivity +import com.gh.gamecenter.gamedetail.fuli.kaifu.ServersSubscribedGameListActivity import com.gh.gamecenter.gamedetail.history.HistoryApkListActivity import com.gh.gamecenter.gamedetail.rating.RatingReplyActivity import com.gh.gamecenter.help.HelpAndFeedbackBridge @@ -192,6 +194,8 @@ object DirectUtils { } } + "game_server_calendar" -> directToGameServerCalendar(context, linkEntity.link) + "column", "游戏专题" -> directToSubject( context, linkEntity.link ?: "", linkEntity.text, BaseActivity.mergeEntranceAndPath(entrance, path), exposureEvent @@ -543,12 +547,13 @@ object DirectUtils { * 跳转至游戏日历表 */ @JvmStatic - fun directToGameServerCalendar(context: Context, gameId: String?) { + fun directToGameServerCalendar(context: Context, gameId: String?, kaifuTime: Long = 0) { val bundle = Bundle() bundle.putString(KEY_TO, ServersCalendarActivity::class.java.name) bundle.putParcelable(GameEntity::class.java.simpleName, GameEntity().apply { id = gameId ?: "" }) + bundle.putLong(KEY_KAIFU_TIME, kaifuTime) bundle.putParcelable(GameDetailServer::class.java.simpleName, GameDetailServer()) bundle.putParcelable(MeEntity::class.java.simpleName, MeEntity()) jumpActivity(context, bundle) @@ -2117,4 +2122,24 @@ object DirectUtils { ) ) } + + /** + * 跳转到游戏订阅页面 + * @param context 上下文 + */ + @JvmStatic + fun directToServersSubscribedGameList(context: Context) { + context.startActivity(ServersSubscribedGameListActivity.getIntent(context)) + } + + /** + * 跳转到开服订阅页面 + * @param context 上下文 + */ + @JvmStatic + fun directToServersCalendarManagement(context: Context, entrance: String) { + CheckLoginUtils.checkLogin(context, entrance) { + context.startActivity(ServersCalendarManagementActivity.getIntent(context)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/common/util/NewLogUtils.kt b/app/src/main/java/com/gh/common/util/NewLogUtils.kt index a2fbe9c0b4..a82aea46f1 100644 --- a/app/src/main/java/com/gh/common/util/NewLogUtils.kt +++ b/app/src/main/java/com/gh/common/util/NewLogUtils.kt @@ -2487,4 +2487,209 @@ object NewLogUtils { } log(json, LOG_STORE_EVENT) } + + /** + * 埋点序号:117 + * 事件ID:message_inform_push + * 事件名称:消息推送事件 + * 触发时机:系统触发消息推送 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param sessionMessageType 会话类型 + * @param messageType 消息类型 + */ + @JvmStatic + fun logMessageInformPush( + gameId: String, + gameName: String, + sessionMessageType: String, + messageType: String + ) { + val json = json { + KEY_EVENT to "message_inform_push" + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + "session_message_type" to sessionMessageType + "message_type" to messageType + parseAndPutMeta().invoke(this) + } + log(json, LOG_STORE_EVENT) + } + + /** + * 埋点序号:118 + * 事件ID:launch_server_subscribe_add + * 事件名称:加入开服订阅事件 + * 触发时机:点击开服表加入订阅 + * @param gameId 游戏ID + * @param gameName 游戏名称 + */ + @JvmStatic + fun logLaunchServerSubscribeAdd( + gameId: String, + gameName: String + ) { + val json = json { + KEY_EVENT to "launch_server_subscribe_add" + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + parseAndPutMeta().invoke(this) + } + log(json, LOG_STORE_EVENT) + } + + /** + * 埋点序号:119 + * 事件ID:launch_server_subscribe_cancel + * 事件名称:取消开服订阅事件 + * 触发时机:点击开服表取消订阅 + * @param gameId 游戏ID + * @param gameName 游戏名称 + */ + @JvmStatic + fun logLaunchServerSubscribeCancel( + gameId: String, + gameName: String + ) { + val json = json { + KEY_EVENT to "launch_server_subscribe_cancel" + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + parseAndPutMeta().invoke(this) + } + log(json, LOG_STORE_EVENT) + } + + /** + * 埋点序号:120 + * 事件ID:launch_server_subscribe_click + * 事件名称:点击开服订阅事件 + * 触发时机:点击开服管理的开服记录 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param launchServerTime 以秒为单位的时间戳,若为日期则是当天0点的时间戳 + */ + @JvmStatic + fun logLaunchServerSubscribeClick( + gameId: String, + gameName: String, + launchServerTime: Int + ) { + val json = json { + KEY_EVENT to "launch_server_subscribe_click" + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + "launch_server_time" to launchServerTime + parseAndPutMeta().invoke(this) + } + log(json, LOG_STORE_EVENT) + } + + /** + * 埋点序号:121 + * 事件ID:launch_server_reminder_add + * 事件名称:添加开服提醒事件 + * 触发时机:点击开服表的添加提醒 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param reminderTime 设置提醒时间: 已知服是上传以秒为单位的时间戳,未知服则是当天0点的时间戳 + * @param reminderType 提醒类型: 已知服/未知服 + * @param launchServerTime 设置开服时间: 已知服是上传以秒为单位的时间戳,未知服则是当天0点的时间戳 + * @param serverName 服务器名字 + * @param isWechat 是否微信提醒 + * @param isApp 是否APP提醒 + */ + @JvmStatic + fun logLaunchServerReminderAdd( + gameId: String, + gameName: String, + reminderTime: Int, + reminderType: String, + launchServerTime: Int, + serverName: String, + isWechat: Boolean, + isApp: Boolean + ) { + val json = json { + KEY_EVENT to "launch_server_reminder_add" + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + "reminder_time" to reminderTime + "reminder_type" to reminderType + "launch_server_time" to launchServerTime + "server_name" to serverName + "is_wechat" to isWechat + "is_app" to isApp + parseAndPutMeta().invoke(this) + } + log(json, LOG_STORE_EVENT) + } + + /** + * 埋点序号:122 + * 事件ID:launch_server_reminder_cancel + * 事件名称:取消开服提醒事件 + * 触发时机:点击开服表的取消提醒 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param reminderTime 设置提醒时间: 已知服是上传以秒为单位的时间戳,未知服则是当天0点的时间戳 + * @param reminderType 提醒类型: 已知服/未知服 + * @param launchServerTime 设置开服时间: 已知服是上传以秒为单位的时间戳,未知服则是当天0点的时间戳 + * @param serverName 服务器名字 + * @param isWechat 是否微信提醒 + * @param isApp 是否APP提醒 + */ + @JvmStatic + fun logLaunchServerReminderCancel( + gameId: String, + gameName: String, + reminderTime: Int, + reminderType: String, + launchServerTime: Int, + serverName: String, + isWechat: Boolean, + isApp: Boolean + ) { + val json = json { + KEY_EVENT to "launch_server_reminder_cancel" + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + "reminder_time" to reminderTime + "reminder_type" to reminderType + "launch_server_time" to launchServerTime + "server_name" to serverName + "is_wechat" to isWechat + "is_app" to isApp + parseAndPutMeta().invoke(this) + } + log(json, LOG_STORE_EVENT) + } + + /** + * 埋点序号:123 + * 事件ID:launch_server_reminder_click + * 事件名称:点击开服提醒事件 + * 触发时机:点击开服管理的提醒记录 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param reminderTime 设置提醒时间: 已知服是上传以秒为单位的时间戳,未知服则是当天0点的时间戳 + * @param status 记录状态: todo 待提醒 coming 即将开服 opened 已开服 has_new 有新服 expired 已过期 + */ + @JvmStatic + fun logLaunchServerReminderClick( + gameId: String, + gameName: String, + reminderTime: Int, + status: String, + ) { + val json = json { + KEY_EVENT to "launch_server_reminder_click" + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + "reminder_time" to reminderTime + "status" to status + parseAndPutMeta().invoke(this) + } + log(json, LOG_STORE_EVENT) + } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/SkipActivity.java b/app/src/main/java/com/gh/gamecenter/SkipActivity.java index 83a6693dc1..f7f09af1e1 100644 --- a/app/src/main/java/com/gh/gamecenter/SkipActivity.java +++ b/app/src/main/java/com/gh/gamecenter/SkipActivity.java @@ -385,7 +385,7 @@ public class SkipActivity extends BaseActivity { EntranceConsts.ENTRANCE_BROWSER); break; case EntranceConsts.HOST_GAME_CALENDAR: - DirectUtils.directToGameServerCalendar(this, uri.getQueryParameter(EntranceConsts.KEY_GAME_ID)); + DirectUtils.directToGameServerCalendar(this, uri.getQueryParameter(EntranceConsts.KEY_GAME_ID), 0); break; case EntranceConsts.HOST_HISTORY_APK: DirectUtils.directToHistoryApk(this, uri.getQueryParameter(EntranceConsts.KEY_GAME_ID)); diff --git a/app/src/main/java/com/gh/gamecenter/entity/CalendarEntity.kt b/app/src/main/java/com/gh/gamecenter/entity/CalendarEntity.kt index 3b59116802..60dd504998 100644 --- a/app/src/main/java/com/gh/gamecenter/entity/CalendarEntity.kt +++ b/app/src/main/java/com/gh/gamecenter/entity/CalendarEntity.kt @@ -1,15 +1,16 @@ package com.gh.gamecenter.entity +import android.os.Parcelable import com.gh.gamecenter.feature.entity.ServerCalendarEntity +import kotlinx.parcelize.Parcelize /** * Created by khy on 2017/3/28. */ -class CalendarEntity { - - var day: Int = 0 - var month: Int = 0 - var year: Int = 0 - +@Parcelize +class CalendarEntity( + var day: Int = 0, + var month: Int = 0, + var year: Int = 0, var server: MutableList = ArrayList() -} +) : Parcelable diff --git a/app/src/main/java/com/gh/gamecenter/entity/ServerSubscriptionEntity.kt b/app/src/main/java/com/gh/gamecenter/entity/ServerSubscriptionEntity.kt new file mode 100644 index 0000000000..9890f80767 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/entity/ServerSubscriptionEntity.kt @@ -0,0 +1,10 @@ +package com.gh.gamecenter.entity + +import com.google.gson.annotations.SerializedName + +class ServerSubscriptionEntity( + @SerializedName("by_app") + val byApp: Boolean = false, + @SerializedName("by_wechat") + val byWeChat: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/fragment/SearchToolbarFragment.java b/app/src/main/java/com/gh/gamecenter/fragment/SearchToolbarFragment.java index 77e6a584f3..a304fe4a4d 100644 --- a/app/src/main/java/com/gh/gamecenter/fragment/SearchToolbarFragment.java +++ b/app/src/main/java/com/gh/gamecenter/fragment/SearchToolbarFragment.java @@ -37,6 +37,7 @@ import com.gh.gamecenter.common.base.fragment.BaseLazyFragment; import com.gh.gamecenter.common.constant.Constants; import com.gh.gamecenter.common.constant.EntranceConsts; import com.gh.gamecenter.common.eventbus.EBReuse; +import com.gh.gamecenter.common.utils.SensorsBridge; import com.gh.gamecenter.core.iinterface.SearchBarHint; import com.gh.gamecenter.core.utils.DisplayUtils; import com.gh.gamecenter.core.utils.MtaHelper; @@ -326,7 +327,7 @@ public class SearchToolbarFragment extends BaseLazyFragment implements View.OnCl CheckLoginUtils.checkLogin(requireContext(), "(工具栏)", () -> { NewLogUtils.logMessageInformBellClick(mMessageUnread.getVisibility() == View.VISIBLE, mLocation); - + SensorsBridge.trackMessageCenterClick(); // 优先进入有数字提醒的消息tab,其次是有红点提醒的游戏动态,最后是没有提醒的消息tab int defaultTabIndex = 1; MessageUnreadCount messageUnreadCount = mUnreadViewModel.getMessageUnreadCountLiveData().getValue(); diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/GameDetailFragment.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/GameDetailFragment.kt index 5faaf654d8..422ed9746f 100644 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/GameDetailFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/GameDetailFragment.kt @@ -1648,7 +1648,7 @@ class GameDetailFragment : ToolbarFragment(), IScrollable { true ) updateConcernMenuIcon(mNewGameDetailEntity?.me?.isGameConcerned ?: false) - mMoreMenuItem?.setIcon(if (!mIsDarkModeOn && isToolbarWhite) R.drawable.ic_menu_gamedetail_more else R.drawable.ic_menu_gamedetail_more_light) + mMoreMenuItem?.setIcon(if (!mIsDarkModeOn && isToolbarWhite) R.drawable.ic_menu_more else R.drawable.ic_menu_more_light) mSearchMenuItem?.setIcon(if (!mIsDarkModeOn && isToolbarWhite) R.drawable.ic_column_search else R.drawable.ic_column_search_light) mDownloadMenuIcon?.setImageResource(if (!mIsDarkModeOn && isToolbarWhite) R.drawable.toolbar_download else R.drawable.toolbar_download_light) DisplayUtils.setLightStatusBar(requireActivity(), !mIsDarkModeOn && isToolbarWhite) diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarActivity.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarActivity.kt index 7681d450f3..ce75678d85 100644 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarActivity.kt +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarActivity.kt @@ -5,36 +5,33 @@ import android.app.Dialog import android.content.Context import android.content.Intent import android.graphics.Color -import android.graphics.Paint import android.os.Bundle import android.text.TextUtils -import android.view.Gravity import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView +import com.gh.common.util.CheckLoginUtils import com.gh.common.util.NewLogUtils import com.gh.gamecenter.R import com.gh.gamecenter.common.base.activity.ToolBarActivity -import com.gh.gamecenter.common.entity.SuggestType +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.eventbus.EBReuse import com.gh.gamecenter.common.utils.* import com.gh.gamecenter.core.utils.SPUtils +import com.gh.gamecenter.core.utils.ToastUtils import com.gh.gamecenter.databinding.ActivityServersCalendarBinding -import com.gh.gamecenter.databinding.DialogServersCalendearDetailItemBinding import com.gh.gamecenter.entity.CalendarEntity import com.gh.gamecenter.feature.entity.GameDetailServer import com.gh.gamecenter.feature.entity.GameEntity import com.gh.gamecenter.feature.entity.MeEntity import com.gh.gamecenter.feature.entity.ServerCalendarEntity -import com.gh.gamecenter.help.HelpAndFeedbackBridge -import com.gh.gamecenter.servers.add.AddKaiFuActivity -import com.google.android.material.bottomsheet.BottomSheetDialog import com.halo.assistant.HaloApp -import com.lightgame.adapter.BaseRecyclerAdapter +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import java.text.SimpleDateFormat import java.util.* @@ -46,6 +43,13 @@ class ServersCalendarActivity : ToolBarActivity() { NewLogUtils.logGameDetailOpenCalendarView(mViewModel.game.name ?: "", mViewModel.game.id) } + private val mMoreDialog by lazy { ServersCalendarMoreDialog() } + + private val mWeChatSubscriptionSuccessDialog by lazy { ServersCalendarWeChatSubscriptionSuccessDialog() } + + private val mSubscriptionSuccessDialog by lazy { ServersCalendarSubscriptionSuccessDialog() } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) val serverCalendars = mViewModel.serverCalendarLiveData.value ?: return @@ -85,6 +89,20 @@ class ServersCalendarActivity : ToolBarActivity() { } override fun onCreate(savedInstanceState: Bundle?) { + val game = intent?.getParcelableExtra(GameEntity::class.java.simpleName) + val serverList = intent?.getParcelableExtra(GameDetailServer::class.java.simpleName) + val meEntity = intent?.getParcelableExtra(MeEntity::class.java.simpleName) + if (game == null || serverList == null) { + throwExceptionInDebug() + return + } + + // 必须放在super.onCreate前面,因为子Fragment的onCreate方法也是在super.onCreate回调的时候调用 + // 如果放在super.onCreate之后,那么在页面重构时,子Fragment在onCreate方法引用通过viewModels方法获取parentViewModel将触发崩溃 + val factory = ServersCalendarViewModel.Factory(HaloApp.getInstance().application, game, serverList, meEntity) + mViewModel = ViewModelProviders.of(this, factory).get(ServersCalendarViewModel::class.java) + mViewModel.setKaifuTime(intent.getLongExtra(EntranceConsts.KEY_KAIFU_TIME, 0)) + super.onCreate(savedInstanceState) updateStatusBarColor(R.color.background_white, R.color.background_white) setNavigationTitle("开服日历表") @@ -96,16 +114,6 @@ class ServersCalendarActivity : ToolBarActivity() { mViewModel.loadServerData() } - val game = intent?.getParcelableExtra(GameEntity::class.java.simpleName) - val serverList = intent?.getParcelableExtra(GameDetailServer::class.java.simpleName) - val meEntity = intent?.getParcelableExtra(MeEntity::class.java.simpleName) - if (game == null || serverList == null) { - throwExceptionInDebug() - return - } - - val factory = ServersCalendarViewModel.Factory(HaloApp.getInstance().application, game, serverList, meEntity) - mViewModel = ViewModelProviders.of(this, factory).get(ServersCalendarViewModel::class.java) mViewModel.calendarLiveData.observeNonNull(this, callback = { val adapter = ServersCalendarAdapter(this, mViewModel, it) mBinding.recyclerView.isNestedScrollingEnabled = false @@ -113,7 +121,7 @@ class ServersCalendarActivity : ToolBarActivity() { mBinding.recyclerView.adapter = adapter }) mViewModel.serversDetailLiveData.observeNonNull(this, callback = { - showServersDetailDialog(it) + showServersCalendarDetailDialog(it) NewLogUtils.logGameDetailOpenCalendarClick(mViewModel.game.name ?: "", mViewModel.game.id) }) mViewModel.serverCalendarLiveData.observe(this) { @@ -125,6 +133,52 @@ class ServersCalendarActivity : ToolBarActivity() { mBinding.includeNoConnection.root.visibility = View.VISIBLE } } + mViewModel.serverSubscriptionLiveData.observe(this) { + // 用户订阅状态 + if (it?.byApp == true) {// 用户已登录并处于已订阅状态 + clearMenu() + setToolbarMenu(R.menu.menu_server_calendar_more) + mBinding.subscribe.visibility = View.GONE + mBinding.subscribeHint1.visibility = View.GONE + mBinding.subscribeHint2.visibility = View.GONE + mBinding.subscribe.setOnClickListener(null) + } else {// 用户已登录并处于未订阅状态 + clearMenu() + mBinding.subscribe.visibility = View.VISIBLE + mBinding.subscribeHint1.visibility = View.VISIBLE + mBinding.subscribeHint2.visibility = View.VISIBLE + mBinding.subscribe.setOnClickListener { + CheckLoginUtils.checkLogin(this, "游戏详情-开服日历表-开启订阅") { + mViewModel.subscribeServer() + NewLogUtils.logLaunchServerSubscribeAdd( + gameId = mViewModel.game.id, + gameName = mViewModel.game.name ?: "" + ) + SensorsBridge.trackLaunchServerSubscribeClick( + gameId = mViewModel.game.id, + gameName = mViewModel.game.name ?: "" + ) + } + } + } + } + mViewModel.subscribeServerSuccessEvent.observe(this) { + val serverSubscription = mViewModel.serverSubscriptionLiveData.value + if (serverSubscription?.byWeChat != true && !mWeChatSubscriptionSuccessDialog.isAdded) {// 开启微信提醒弹窗 + mWeChatSubscriptionSuccessDialog.showNow( + supportFragmentManager, + SERVERS_CALENDAR_WECHAT_SUBSCRIPTION_DIALOG_TAG + ) + } else if (!mSubscriptionSuccessDialog.isAdded) {// 开启订阅成功提醒弹窗 + mSubscriptionSuccessDialog.showNow( + supportFragmentManager, + SERVERS_CALENDAR_SUBSCRIPTION_DIALOG_TAG + ) + } + } + mViewModel.error.observe(this) { + ToastUtils.showToast(getString(R.string.network_error_hint)) + } mBaseHandler.postDelayed(mDelayLogRunnable, 3000) } @@ -133,6 +187,15 @@ class ServersCalendarActivity : ToolBarActivity() { mBaseHandler.removeCallbacks(mDelayLogRunnable) } + override fun onMenuItemClick(item: MenuItem?): Boolean { + if (item?.itemId == R.id.menu_more) { + if (!mMoreDialog.isAdded) { + mMoreDialog.showNow(supportFragmentManager, SERVERS_CALENDAR_MORE_DIALOG_TAG) + } + } + return super.onMenuItemClick(item) + } + fun initView() { mBinding.contentContainer.visibility = View.VISIBLE mBinding.includeLoading.root.visibility = View.GONE @@ -184,7 +247,7 @@ class ServersCalendarActivity : ToolBarActivity() { } - if (!mViewModel.isExistCurServer) { + if (!mViewModel.isExistCurServer) {// 这里已经不执行了,估计是遗留下的旧代码... mViewModel.selectedMonth = MonthType.NEXT_MONTH mBinding.curMonth.visibility = View.GONE @@ -193,13 +256,41 @@ class ServersCalendarActivity : ToolBarActivity() { mBinding.nextMonth.setTextColor(Color.WHITE) mBinding.year.text = formatYearText.format(previousTime) } else { - mViewModel.selectedMonth = MonthType.CUR_MONTH - mBinding.curMonth.visibility = View.VISIBLE - mBinding.curMonth.setBackgroundResource(R.drawable.download_oval_hint_up) mBinding.curMonth.text = formatMonthText.format(curTime) mBinding.year.text = formatYearText.format(curTime) + + when (mViewModel.selectedMonth) { + MonthType.CUR_MONTH -> { + mBinding.curMonth.setBackgroundResource(R.drawable.download_oval_hint_up) + mBinding.curMonth.setTextColor(Color.WHITE) + + mBinding.previousMonth.setBackgroundColor(0) + mBinding.previousMonth.setTextColor(R.color.text_title.toColor(this)) + mBinding.nextMonth.setBackgroundColor(0) + mBinding.nextMonth.setTextColor(R.color.text_title.toColor(this)) + } + MonthType.NEXT_MONTH -> { + mBinding.nextMonth.setBackgroundResource(R.drawable.download_oval_hint_up) + mBinding.nextMonth.setTextColor(Color.WHITE) + + mBinding.curMonth.setBackgroundColor(0) + mBinding.curMonth.setTextColor(R.color.text_title.toColor(this)) + mBinding.previousMonth.setBackgroundColor(0) + mBinding.previousMonth.setTextColor(R.color.text_title.toColor(this)) + } + else -> { + mBinding.previousMonth.setBackgroundResource(R.drawable.download_oval_hint_up) + mBinding.previousMonth.setTextColor(Color.WHITE) + + mBinding.nextMonth.setBackgroundColor(0) + mBinding.nextMonth.setTextColor(R.color.text_title.toColor(this)) + mBinding.curMonth.setBackgroundColor(0) + mBinding.curMonth.setTextColor(R.color.text_title.toColor(this)) + } + } + } mBinding.nextMonth.setOnClickListener { v -> @@ -276,129 +367,51 @@ class ServersCalendarActivity : ToolBarActivity() { } - private fun showServersDetailDialog(calendarEntity: CalendarEntity) { - val contentView = LayoutInflater.from(this).inflate(R.layout.dialog_servers_calendear_detail, null) - val params = ViewGroup.LayoutParams(resources.displayMetrics.widthPixels, ViewGroup.LayoutParams.WRAP_CONTENT) - val dialog = Dialog(this, R.style.DialogWindowTransparent) - val window = dialog.window - window?.setGravity(Gravity.BOTTOM) - window?.setWindowAnimations(R.style.community_publication_animation) - dialog.setContentView(contentView, params) - dialog.show() + private fun showServersCalendarDetailDialog(calendarEntity: CalendarEntity) { + val currentCalendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + val serverCalendar = Calendar.getInstance().apply { + set(calendarEntity.year, calendarEntity.month - 1, calendarEntity.day, 0, 0, 0) + } - val calendarHint = contentView.findViewById(R.id.calendar_hint) - calendarHint.text = (calendarEntity.server.first().getFormatTime("MM-dd") + "详细开服") + // 开服表详情是否显示提醒按钮,条件如下: + // 2. 当前开服表日期大于当天日期 + val isRemindFeatureSupported = serverCalendar >= currentCalendar - val addBtn = contentView.findViewById(R.id.add) - if (mViewModel.meEntity?.isPartTime == true) { - addBtn.visibility = View.INVISIBLE + if (!isRemindFeatureSupported && calendarEntity.server.isEmpty()) { + // 小于当前时间的未知服不显示弹窗 + return + } + + val dialog = if (calendarEntity.server.isEmpty()) { + ServersCalendarDetailNoDataDialog().apply { + arguments = Bundle().apply { + putInt(EntranceConsts.KEY_CALENDAR_YEAR, calendarEntity.year) + putInt(EntranceConsts.KEY_CALENDAR_MONTH, calendarEntity.month) + putInt(EntranceConsts.KEY_CALENDAR_DAY, calendarEntity.day) + } + } } else { - addBtn.visibility = View.GONE - } - - val feedback = contentView.findViewById(R.id.feedback) - feedback.paint.flags = Paint.UNDERLINE_TEXT_FLAG - feedback.paint.isAntiAlias = true - if (mViewModel.meEntity?.isPartTime == true) { - feedback.text = "新增" - } else { - feedback.text = "意见反馈" - } - - feedback.setOnClickListener { - if (mViewModel.meEntity?.isPartTime == true) { - startActivityForResult( - AddKaiFuActivity.getIntent( - this, - mViewModel.serverCalendarLiveData.value!!.last(), - mViewModel.serverCalendarLiveData.value as ArrayList, - mViewModel.game.id, calendarEntity.server.first().getTime() * 1000 - ), GAME_DETAIL_ADD_KAIFU_REQUEST - ) - } else { - dialog.dismiss() - showServersDetailReportDialog(calendarEntity) - } - } - - contentView.findViewById(R.id.close).setOnClickListener { - dialog.dismiss() - } - - val recyclerView = contentView.findViewById(R.id.recycler_view) - recyclerView.layoutManager = LinearLayoutManager(this) - recyclerView.adapter = object : BaseRecyclerAdapter(this) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServersDetailViewHolder { - val view = mLayoutInflater.inflate(R.layout.dialog_servers_calendear_detail_item, parent, false) - return ServersDetailViewHolder(DialogServersCalendearDetailItemBinding.bind(view)) - } - - override fun getItemCount(): Int { - return calendarEntity.server.size - } - - override fun onBindViewHolder(holder: ServersDetailViewHolder, position: Int) { - val data = calendarEntity.server[position] - holder.bindItem(data, mViewModel.meEntity, mViewModel.game) - - if (mViewModel.meEntity?.isPartTime == true) { - holder.binding.add.visibility = View.VISIBLE - } else { - holder.binding.add.visibility = View.GONE - } - holder.binding.add.setOnClickListener { - startActivityForResult( - AddKaiFuActivity.getIntent( - this@ServersCalendarActivity, - data, - mViewModel.serverCalendarLiveData.value as ArrayList, - mViewModel.game.id, - mViewModel.getSelectTime(calendarEntity.day, calendarEntity.month, calendarEntity.year) - ), ServersCalendarActivity.GAME_DETAIL_ADD_KAIFU_REQUEST - ) - } - if (itemCount == position + 1) { - holder.itemView.setPadding(0, 10F.dip2px(), 0, 19F.dip2px()) - } else { - holder.itemView.setPadding(0, 10F.dip2px(), 0, 10F.dip2px()) + ServersCalendarDetailDialog().apply { + arguments = Bundle().apply { + putInt(EntranceConsts.KEY_CALENDAR_YEAR, calendarEntity.year) + putInt(EntranceConsts.KEY_CALENDAR_MONTH, calendarEntity.month) + putInt(EntranceConsts.KEY_CALENDAR_DAY, calendarEntity.day) + putBoolean(EntranceConsts.KEY_SHOW_REMIND, isRemindFeatureSupported) } } } + dialog.showNow(supportFragmentManager, null) } - private fun showServersDetailReportDialog(calendarEntity: CalendarEntity) { - val contentView = LayoutInflater.from(this).inflate(R.layout.dialog_servers_calendear_detail_report, null) - val params = ViewGroup.LayoutParams(resources.displayMetrics.widthPixels, ViewGroup.LayoutParams.WRAP_CONTENT) - val dialog = BottomSheetDialog(this, R.style.DialogWindowTransparent) - val window = dialog.window - window?.setGravity(Gravity.BOTTOM) - window?.setWindowAnimations(R.style.community_publication_animation) - dialog.setContentView(contentView, params) - dialog.show() - - val recyclerView = contentView.findViewById(R.id.recycler_view) - val adapter = ServersDetailReportAdapter(this, calendarEntity.server) - recyclerView.layoutManager = LinearLayoutManager(this) - recyclerView.adapter = adapter - - contentView.findViewById(R.id.submit).setOnClickListener { - val builder = StringBuilder() - val serverCalendarList = adapter.selectedServerCalendarList - serverCalendarList.forEachIndexed { index, entity -> - if (index != 0) builder.append(";") - builder.append("${entity.getFormatTime("YYYY年MM月dd日")}开服信息有误:${entity.getNote()}") - } - HelpAndFeedbackBridge.startSuggestionActivity( - this, - SuggestType.GAME, - "service", - builder.toString() - ) - dialog.dismiss() - } - - contentView.findViewById(R.id.close).setOnClickListener { - dialog.dismiss() + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(reuse: EBReuse) { + if (reuse.type == Constants.LOGIN_TAG || reuse.type == Constants.LOGOUT_TAG) { + mViewModel.loadServerData() } } @@ -423,6 +436,10 @@ class ServersCalendarActivity : ToolBarActivity() { const val GAME_DETAIL_ADD_KAIFU_KEY = "GAME_DETAIL_ADD_KAIFU_KEY" const val GAME_DETAIL_PATCH_KAIFU_KEY = "GAME_DETAIL_PATCH_KAIFU_KEY" + const val SERVERS_CALENDAR_MORE_DIALOG_TAG = "ServersCalendarMoreDialog" + const val SERVERS_CALENDAR_WECHAT_SUBSCRIPTION_DIALOG_TAG = "ServersCalendarWeChatSubscriptionDialog" + const val SERVERS_CALENDAR_SUBSCRIPTION_DIALOG_TAG = "ServersCalendarSubscriptionDialog" + fun getIntent(context: Context, game: GameEntity, gameServer: GameDetailServer, me: MeEntity?): Intent { val intent = Intent(context, ServersCalendarActivity::class.java) intent.putExtra(GameEntity::class.java.simpleName, game) diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarAdapter.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarAdapter.kt index 221d9f1279..12a1b38d2f 100644 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarAdapter.kt @@ -119,6 +119,8 @@ class ServersCalendarAdapter( ), ServersCalendarActivity.GAME_DETAIL_ADD_KAIFU_REQUEST ) + } else { + viewModel.serversDetailLiveData.postValue(entity) } } else { MtaHelper.onEvent("开服日历表", "详情", "${viewModel.game.name}+${holder.binding.calendarKaifu.text}") diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarAdvancedTime.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarAdvancedTime.kt new file mode 100644 index 0000000000..2540b1b04f --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarAdvancedTime.kt @@ -0,0 +1,18 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +enum class ServersCalendarAdvancedTime(val value: Int, val text: String) { + IN_TIME(0, "准时提醒"), + FIVE_MINUTES_IN_ADVANCE(300, "提前5分钟"), + FIFTEEN_MINUTES_IN_ADVANCE(900, "提前15分钟"), + THIRTY_MINUTES_IN_ADVANCE(1800, "提前30分钟"); + + companion object { + fun valueOf(value: Int?): ServersCalendarAdvancedTime { + if (value == null) return IN_TIME + + return ServersCalendarAdvancedTime.values().firstOrNull { + it.value == value + } ?: IN_TIME + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailAdapter.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailAdapter.kt new file mode 100644 index 0000000000..14d8fb819d --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailAdapter.kt @@ -0,0 +1,151 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Activity +import android.graphics.Paint +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import com.gh.common.util.CheckLoginUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.BaseRecyclerViewHolder +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.layoutInflater +import com.gh.gamecenter.common.utils.toColor +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.databinding.DialogServersCalendearDetailItemBinding +import com.gh.gamecenter.feature.entity.GameEntity +import com.gh.gamecenter.feature.entity.MeEntity +import com.gh.gamecenter.feature.entity.ServerCalendarEntity +import com.gh.gamecenter.servers.add.AddKaiFuActivity +import com.gh.gamecenter.servers.patch.PatchKaifuActivity +import com.lightgame.utils.Utils +import java.util.* + +class ServersCalendarDetailAdapter( + private val fragment: Fragment, + private val viewModel: ServersCalendarDetailViewModel, + private val parentViewModel: ServersCalendarViewModel +) : ListAdapter( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ServerCalendarEntity, newItem: ServerCalendarEntity): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: ServerCalendarEntity, newItem: ServerCalendarEntity): Boolean { + return oldItem.id == newItem.id + && oldItem.getFirstName() == newItem.getFirstName() + && oldItem.getServerName() == newItem.getServerName() + && oldItem.getNote() == newItem.getNote() + && oldItem.getTime() == newItem.getTime() + && oldItem.remark == newItem.remark + && oldItem.type == newItem.type + && oldItem.notifySetting?.notifyTime == newItem.notifySetting?.notifyTime + && oldItem.notifySetting?.secondsAdvance == newItem.notifySetting?.secondsAdvance + && oldItem.notifySetting?.byApp == newItem.notifySetting?.byApp + && oldItem.notifySetting?.byWechat == newItem.notifySetting?.byWechat + + } + } +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServersCalendarDetailViewHolder { + return ServersCalendarDetailViewHolder( + DialogServersCalendearDetailItemBinding.inflate( + parent.layoutInflater, + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ServersCalendarDetailViewHolder, position: Int) { + + val data = getItem(position) + + holder.bindItem(data, parentViewModel.meEntity, parentViewModel.game) + + if (parentViewModel.meEntity?.isPartTime == true) { + holder.binding.add.visibility = View.VISIBLE + } else { + holder.binding.add.visibility = View.GONE + } + holder.binding.add.setOnClickListener { + fragment.startActivityForResult( + AddKaiFuActivity.getIntent( + it.context, + data, + parentViewModel.serverCalendarLiveData.value as ArrayList, + parentViewModel.game.id, + parentViewModel.getSelectTime( + viewModel.year, + viewModel.month, + viewModel.day + ) + ), ServersCalendarActivity.GAME_DETAIL_ADD_KAIFU_REQUEST + ) + } + + holder.itemView.layoutParams = (holder.itemView.layoutParams as ViewGroup.MarginLayoutParams).apply { + bottomMargin = if (itemCount == position + 1) 7F.dip2px() else 0 + } + + holder.binding.remindTitle.visibility = View.GONE + + holder.binding.remind.visibility = if (viewModel.showRemind) View.VISIBLE else View.GONE + + holder.binding.remind.isSelected = data.notifySetting != null + + holder.binding.remind.setOnClickListener { + CheckLoginUtils.checkLogin(it.context, "游戏详情-开服日历表-开服详情-开服提醒") { + if ((System.currentTimeMillis() - (data.getTime() * 1000)) > 0) { + ToastUtils.showToast(it.context.getString(R.string.servers_calendar_time_out_of_date)) + return@checkLogin + } + ServersCalendarDetailsRemindDialog().apply { + arguments = Bundle().apply { + putString(EntranceConsts.KEY_SERVER_CALENDAR_ID, data.id) + } + showNow(fragment.childFragmentManager, null) + } + } + } + } + + class ServersCalendarDetailViewHolder(val binding: DialogServersCalendearDetailItemBinding) : + BaseRecyclerViewHolder(binding.root) { + + fun bindItem(data: ServerCalendarEntity, meEntity: MeEntity?, gameEntity: GameEntity) { + binding.name.text = data.getNote() + binding.time.text = data.getFormatTime("HH:mm") + binding.remark.text = data.remark + + binding.name.setTextColor(R.color.text_title.toColor(binding.root.context)) + binding.time.setTextColor(R.color.text_title.toColor(binding.root.context)) + binding.remark.setTextColor(R.color.text_title.toColor(binding.root.context)) + + if (meEntity?.isPartTime == true) { + binding.name.paint.flags = Paint.UNDERLINE_TEXT_FLAG + binding.name.paint.isAntiAlias = true + binding.name.setOnClickListener { + val context = binding.root.context + if ("删档内测" == data.type || "不删档内测" == data.type || "公测" == data.type) { + Utils.toast(context, "开测信息不可编辑"); + } else { + (context as Activity).startActivityForResult( + PatchKaifuActivity.getIntent( + context, + data, + gameEntity.id + ), + ServersCalendarActivity.GAME_DETAIL_PATCH_KAIFU_REQUEST + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailDialog.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailDialog.kt new file mode 100644 index 0000000000..d145bf2f8e --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailDialog.kt @@ -0,0 +1,164 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Dialog +import android.graphics.Paint +import android.os.Bundle +import android.view.* +import android.widget.TextView +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.gh.gamecenter.R +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.entity.SuggestType +import com.gh.gamecenter.common.utils.viewModelProvider +import com.gh.gamecenter.databinding.DialogServersCalendearDetailBinding +import com.gh.gamecenter.databinding.LayoutServersCalendarDetailListBinding +import com.gh.gamecenter.entity.CalendarEntity +import com.gh.gamecenter.feature.entity.ServerCalendarEntity +import com.gh.gamecenter.help.HelpAndFeedbackBridge +import com.gh.gamecenter.servers.add.AddKaiFuActivity +import com.google.android.material.bottomsheet.BottomSheetDialog +import java.text.SimpleDateFormat +import java.util.* + +/** + * 游戏开服-开服表详情弹窗 + */ +class ServersCalendarDetailDialog : DialogFragment() { + + private lateinit var detailViewBinding: DialogServersCalendearDetailBinding + + private lateinit var listViewBinding: LayoutServersCalendarDetailListBinding + + private val titleFormat = SimpleDateFormat("MM-dd", Locale.CHINA) + + private val parentViewModel by activityViewModels() + + private lateinit var viewModel: ServersCalendarDetailViewModel + + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + window?.setWindowAnimations(R.style.community_publication_animation) + window?.setGravity(Gravity.BOTTOM) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + // 必须放在super.onCreate前面,因为子Fragment的onCreate方法也是在super.onCreate回调的时候调用 + // 如果放在super.onCreate之后,那么在页面重构时,子Fragment在onCreate方法引用通过viewModels方法获取parentViewModel将触发崩溃 + viewModel = viewModelProvider( + ServersCalendarDetailViewModel.Factory( + source = parentViewModel.calendarLiveData, + year = requireArguments().getInt(EntranceConsts.KEY_CALENDAR_YEAR, 0), + month = requireArguments().getInt(EntranceConsts.KEY_CALENDAR_MONTH, 0), + day = requireArguments().getInt(EntranceConsts.KEY_CALENDAR_DAY, 0), + showRemind = requireArguments().getBoolean(EntranceConsts.KEY_SHOW_REMIND, false) + ) + ) + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, R.style.DialogWindowTransparent) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return DialogServersCalendearDetailBinding.inflate(inflater, container, false) + .also { + detailViewBinding = it.apply { + content.layoutResource = R.layout.layout_servers_calendar_detail_list + listViewBinding = LayoutServersCalendarDetailListBinding.bind(content.inflate()) + } + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // 在这里加这句代码,才能保证弹窗宽度占满屏幕 + // 如果在弹窗的Theme里面加false,那么这句代码可以去掉 + dialog?.window?.setLayout( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.WRAP_CONTENT + ) + + val isPartTime = parentViewModel.meEntity?.isPartTime == true + + listViewBinding.titleInclude.add.visibility = if (isPartTime) View.VISIBLE else View.GONE + + listViewBinding.titleInclude.remindTitle.visibility = if (viewModel.showRemind) View.VISIBLE else View.GONE + + detailViewBinding.close.setOnClickListener { dismissAllowingStateLoss() } + + detailViewBinding.feedback.paint.flags = Paint.UNDERLINE_TEXT_FLAG + detailViewBinding.feedback.paint.isAntiAlias = true + detailViewBinding.feedback.text = if (isPartTime) "新增" else "意见反馈" + detailViewBinding.feedback.setOnClickListener { + val calendarEntity = viewModel.calendarEntityLiveData.value ?: return@setOnClickListener + if (isPartTime) { + startActivityForResult( + AddKaiFuActivity.getIntent( + it.context, + parentViewModel.serverCalendarLiveData.value!!.last(), + parentViewModel.serverCalendarLiveData.value as ArrayList, + parentViewModel.game.id, calendarEntity.server.first().getTime() * 1000 + ), ServersCalendarActivity.GAME_DETAIL_ADD_KAIFU_REQUEST + ) + } else { + showServersDetailReportDialog(calendarEntity) + dismissAllowingStateLoss() + } + } + + val adapter = ServersCalendarDetailAdapter(this@ServersCalendarDetailDialog, viewModel, parentViewModel) + listViewBinding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + listViewBinding.recyclerView.adapter = adapter + + viewModel.calendarEntityLiveData.observe(viewLifecycleOwner) { calendarEntity -> + calendarEntity?.let { + val calendar = Calendar.getInstance().apply { + set(it.year, it.month - 1, it.day) + } + detailViewBinding.calendarHint.text = "${titleFormat.format(calendar.time)}详细开服" + adapter.submitList(calendarEntity.server) + } + } + } + + private fun showServersDetailReportDialog(calendarEntity: CalendarEntity) { + val contentView = + LayoutInflater.from(requireContext()).inflate(R.layout.dialog_servers_calendear_detail_report, null) + val params = ViewGroup.LayoutParams(resources.displayMetrics.widthPixels, ViewGroup.LayoutParams.WRAP_CONTENT) + val dialog = BottomSheetDialog(requireContext(), R.style.DialogWindowTransparent) + val window = dialog.window + window?.setGravity(Gravity.BOTTOM) + window?.setWindowAnimations(R.style.community_publication_animation) + dialog.setContentView(contentView, params) + dialog.show() + + val recyclerView = contentView.findViewById(R.id.recycler_view) + val adapter = ServersCalendarDetailReportAdapter(requireContext(), calendarEntity.server) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + recyclerView.adapter = adapter + + contentView.findViewById(R.id.submit).setOnClickListener { + val builder = StringBuilder() + val serverCalendarList = adapter.selectedServerCalendarList + serverCalendarList.forEachIndexed { index, entity -> + if (index != 0) builder.append(";") + builder.append("${entity.getFormatTime("YYYY年MM月dd日")}开服信息有误:${entity.getNote()}") + } + HelpAndFeedbackBridge.startSuggestionActivity( + it.context, + SuggestType.GAME, + "service", + builder.toString() + ) + dialog.dismiss() + } + + contentView.findViewById(R.id.close).setOnClickListener { + dialog.dismiss() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailNoDataDialog.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailNoDataDialog.kt new file mode 100644 index 0000000000..947e9db670 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailNoDataDialog.kt @@ -0,0 +1,281 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Dialog +import android.graphics.Paint +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.activityViewModels +import com.gh.common.constant.Config +import com.gh.common.util.CheckLoginUtils +import com.gh.common.util.NewLogUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.entity.SuggestType +import com.gh.gamecenter.common.eventbus.EBReuse +import com.gh.gamecenter.common.utils.SensorsBridge +import com.gh.gamecenter.common.utils.WXAPIProxyFactory +import com.gh.gamecenter.common.utils.viewModelProvider +import com.gh.gamecenter.common.view.DrawableView +import com.gh.gamecenter.core.utils.SPUtils +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.databinding.DialogServersCalendearDetailBinding +import com.gh.gamecenter.databinding.LayoutServersCalendarDetailNoDataBinding +import com.gh.gamecenter.feature.entity.ServerCalendarNotifySetting +import com.gh.gamecenter.help.HelpAndFeedbackBridge +import com.gh.gamecenter.login.user.UserManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.tencent.mm.opensdk.modelbase.BaseResp +import com.tencent.mm.opensdk.modelbiz.SubscribeMessage +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.text.SimpleDateFormat +import java.util.* + +/** + * 游戏开服-未知服详情弹窗 + */ +class ServersCalendarDetailNoDataDialog : BottomSheetDialogFragment() { + + private val titleFormat = SimpleDateFormat("MM-dd", Locale.CHINA) + + private lateinit var detailViewBinding: DialogServersCalendearDetailBinding + private lateinit var noDataViewBinding: LayoutServersCalendarDetailNoDataBinding + + private val parentViewModel by activityViewModels() + + private lateinit var viewModel: ServersCalendarDetailNoDataViewModel + + private lateinit var wxApi: WXAPIProxyFactory.WXAPIProxy + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + window?.setWindowAnimations(R.style.community_publication_animation) + window?.setGravity(Gravity.BOTTOM) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + EventBus.getDefault().register(this) + setStyle(DialogFragment.STYLE_NO_TITLE, R.style.DialogWindowTransparent) + Calendar.getInstance().apply { + set( + requireArguments().getInt(EntranceConsts.KEY_CALENDAR_YEAR, 0), + requireArguments().getInt(EntranceConsts.KEY_CALENDAR_MONTH, 0) - 1, + requireArguments().getInt(EntranceConsts.KEY_CALENDAR_DAY, 0), + 0, 0, 0 + ) + set(Calendar.MILLISECOND, 0) + viewModel = viewModelProvider(ServersCalendarDetailNoDataViewModel.Factory(timeInMillis)) + } + wxApi = WXAPIProxyFactory.createWXAPI( + requireContext(), + Config.WECHAT_APPID, + object: WXAPIProxyFactory.WXHandler { + private val reserved = System.currentTimeMillis().toString() + + override fun handleOnReq(req: SubscribeMessage.Req) { + req.reserved = reserved + } + + override fun handleOnResp(resp: BaseResp): SubscribeMessage.Resp? { + if (resp is SubscribeMessage.Resp && reserved == resp.reserved) { + return resp + } + return null + } + } + ) + } + + override fun onDestroy() { + super.onDestroy() + EventBus.getDefault().unregister(this) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return DialogServersCalendearDetailBinding.inflate(inflater, container, false) + .also { + detailViewBinding = it.apply { + content.layoutResource = R.layout.layout_servers_calendar_detail_no_data + noDataViewBinding = LayoutServersCalendarDetailNoDataBinding.bind(content.inflate()) + content.layoutParams = content.layoutParams.apply { + height = LayoutParams.WRAP_CONTENT + } + } + it.container.layoutParams = it.container.layoutParams.apply { + height = LayoutParams.WRAP_CONTENT + } + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + detailViewBinding.calendarHint.text = "${titleFormat.format(viewModel.serverTimeInMills)}详细开服" + + detailViewBinding.feedback.paint.flags = Paint.UNDERLINE_TEXT_FLAG + detailViewBinding.feedback.paint.isAntiAlias = true + detailViewBinding.feedback.text = "意见反馈" + detailViewBinding.feedback.setOnClickListener { + HelpAndFeedbackBridge.startSuggestionActivity( + it.context, + SuggestType.GAME, + "service" + ) + } + + detailViewBinding.close.setOnClickListener { + dismissAllowingStateLoss() + } + + noDataViewBinding.appRemindCheckIv.setImageDrawable(DrawableView.getCheckSelectorDrawable(requireContext())) + noDataViewBinding.wechatRemindCheckIv.setImageDrawable(DrawableView.getCheckSelectorDrawable(requireContext())) + + viewModel.getServerCalenderRemind(parentViewModel.game.id) + viewModel.serverCalendarRemindLiveData.observe(viewLifecycleOwner) { + bindNoDataView(it) + } + + viewModel.wxSubscribeMsgConfigLiveData.observe(viewLifecycleOwner) { + wxApi.sendReq( + SubscribeMessage.Req().apply { + scene = it.scene + templateID = it.templateId + reserved = it.reserved + } + ) + } + + viewModel.error.observe(viewLifecycleOwner) { + ToastUtils.showToast(getString(R.string.network_error_hint)) + } + + wxApi.liveData.observe(viewLifecycleOwner) { + if (it.action == "confirm") { + viewModel.addServerCalendarRemindWithWechat( + gameId = parentViewModel.game.id, + openId = it.openId, + templateId = it.templateID, + action = it.action, + reserved = UserManager.getInstance().userId, + scene = it.scene + ) + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onEventMainThread(reuse: EBReuse) { + if (reuse.type == Constants.LOGIN_TAG || reuse.type == Constants.LOGOUT_TAG) { + viewModel.getServerCalenderRemind(parentViewModel.game.id) + } + } + + private fun bindNoDataView(notifySetting: ServerCalendarNotifySetting?) { + viewModel.initData(notifySetting) + noDataViewBinding.run { + appRemindCheckIv.isChecked = viewModel.appRemind + wechatRemindCheckIv.isChecked = viewModel.wechatRemind + + if (notifySetting != null) { + appRemind.isEnabled = false + appRemindCheckIv.alpha = 0.4F + appRemind.setOnClickListener(null) + + wechatRemind.isEnabled = false + wechatRemindCheckIv.alpha = 0.4F + wechatRemind.setOnClickListener(null) + + addOrCancelRemind.text = getString(R.string.servers_detail_cancel_remind) + addOrCancelRemind.background = + ContextCompat.getDrawable(requireContext(), R.drawable.bg_servers_detail_cancel_no_data) + addOrCancelRemind.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.text_subtitle + ) + ) + addOrCancelRemind.setOnClickListener { + CheckLoginUtils.checkLogin(it.context, "游戏详情-开服日历表-未知服详情") { + viewModel.removeServerCalendarRemind(parentViewModel.game.id) + NewLogUtils.logLaunchServerReminderCancel( + gameId = parentViewModel.game.id, + gameName = parentViewModel.game.name ?: "", + reminderTime = viewModel.timeInSeconds, + reminderType = "未知服", + serverName = "", + launchServerTime = viewModel.timeInSeconds, + isApp = viewModel.appRemind, + isWechat = viewModel.wechatRemind + ) + SensorsBridge.trackLaunchServerReminderCancelClick( + gameId = parentViewModel.game.id, + gameName = parentViewModel.game.name ?: "", + reminderType = "未知服", + ) + } + } + } else { + appRemind.isEnabled = true + appRemindCheckIv.alpha = 1.0F + appRemind.setOnClickListener { + appRemindCheckIv.isChecked = !appRemindCheckIv.isChecked + viewModel.appRemind = appRemindCheckIv.isChecked + SPUtils.setBoolean(Constants.SP_SERVERS_CALENDAR_BY_APP, appRemindCheckIv.isChecked) + } + + wechatRemind.isEnabled = true + wechatRemindCheckIv.alpha = 1.0F + wechatRemind.setOnClickListener { + wechatRemindCheckIv.isChecked = !wechatRemindCheckIv.isChecked + viewModel.wechatRemind = wechatRemindCheckIv.isChecked + SPUtils.setBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, wechatRemindCheckIv.isChecked) + } + + addOrCancelRemind.text = getString(R.string.servers_detail_add_remind) + addOrCancelRemind.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) + addOrCancelRemind.background = + ContextCompat.getDrawable(requireContext(), R.drawable.download_button_normal_style) + addOrCancelRemind.setOnClickListener { + CheckLoginUtils.checkLogin(it.context, "游戏详情-开服日历表-未知服详情") { + if (!viewModel.wechatRemind && !viewModel.appRemind) { + ToastUtils.showToast(getString(R.string.servers_calendar_no_remind_checked_hint)) + return@checkLogin + } + + NewLogUtils.logLaunchServerReminderAdd( + gameId = parentViewModel.game.id, + gameName = parentViewModel.game.name ?: "", + reminderTime = viewModel.timeInSeconds, + reminderType = "未知服", + serverName = "", + launchServerTime = viewModel.timeInSeconds, + isApp = viewModel.appRemind, + isWechat = viewModel.wechatRemind + ) + SensorsBridge.trackLaunchServerReminderClick( + gameId = parentViewModel.game.id, + gameName = parentViewModel.game.name ?: "", + reminderType = "未知服", + ) + + if (viewModel.wechatRemind) {// 勾选微信订阅后需要微信授权一次性订阅消息 + viewModel.getWXSubscribeMsgConfig() + } else { + viewModel.addServerCalendarRemind(parentViewModel.game.id) + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailNoDataViewModel.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailNoDataViewModel.kt new file mode 100644 index 0000000000..3bc5a43452 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailNoDataViewModel.kt @@ -0,0 +1,140 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.annotation.SuppressLint +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.gh.common.util.CheckLoginUtils +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.livedata.NonStickyMutableLiveData +import com.gh.gamecenter.common.utils.toRequestBody +import com.gh.gamecenter.core.utils.SPUtils +import com.gh.gamecenter.core.utils.UrlFilterUtils +import com.gh.gamecenter.feature.entity.ServerCalendarNotifySetting +import com.gh.gamecenter.feature.entity.WXSubscribeMsgConfig +import com.gh.gamecenter.retrofit.RetrofitManager +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class ServersCalendarDetailNoDataViewModel( + val serverTimeInMills: Long +) : ViewModel() { + + val error = NonStickyMutableLiveData() + + val wxSubscribeMsgConfigLiveData = MutableLiveData() + + val serverCalendarRemindLiveData: MutableLiveData = MutableLiveData() + + private var isDataInit = false + + var appRemind: Boolean = false + + var wechatRemind: Boolean = false + + val timeInSeconds: Int = (serverTimeInMills / 1000).toInt() + + fun initData(notifySetting: ServerCalendarNotifySetting?) { + if (isDataInit) return + appRemind = notifySetting?.byApp ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_APP, true) + wechatRemind = notifySetting?.byWechat ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, true) + isDataInit = true + } + + @SuppressLint("CheckResult") + fun getServerCalenderRemind(gameId: String) { + if (!CheckLoginUtils.isLogin()) { + serverCalendarRemindLiveData.postValue(null) + return + } + RetrofitManager.getInstance().newApi + .getServerCalendarRemind(gameId, timeInSeconds.toString()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + serverCalendarRemindLiveData.postValue(it) + }, { + serverCalendarRemindLiveData.postValue(null) + }) + } + + @SuppressLint("CheckResult") + fun addServerCalendarRemind(gameId: String) { + val requestBody = mapOf( + "by_app" to appRemind, + "by_wechat" to wechatRemind + ).toRequestBody() + RetrofitManager.getInstance().newApi + .addServerCalendarRemind(gameId, timeInSeconds.toString(), requestBody) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + getServerCalenderRemind(gameId) + }, { + error.postValue(it) + }) + } + + @SuppressLint("CheckResult") + fun removeServerCalendarRemind(gameId: String) { + RetrofitManager.getInstance().newApi + .removeServerCalendarRemind(gameId, timeInSeconds.toString()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + getServerCalenderRemind(gameId) + }, { + error.postValue(it) + }) + } + + @SuppressLint("CheckResult") + fun getWXSubscribeMsgConfig() { + RetrofitManager.getInstance().newApi + .getWxSubscribeMsgConfig(UrlFilterUtils.getFilterQuery("type", "server_new")) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + wxSubscribeMsgConfigLiveData.postValue(it) + }, { + error.postValue(it) + }) + } + + @SuppressLint("CheckResult") + fun addServerCalendarRemindWithWechat( + gameId: String, + openId: String?, + templateId: String?, + action: String, + reserved: String, + scene: Int, + ) { + val requestBody = mapOf( + "openid" to openId, + "template_id" to templateId, + "action" to action, + "scene" to scene, + "user_id" to reserved + ).toRequestBody() + + RetrofitManager.getInstance().newApi + .postWxSubscribeMsgCallback(UrlFilterUtils.getFilterQuery("type", "server_new"), requestBody) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + addServerCalendarRemind(gameId) + }, { + error.postValue(it) + }) + } + + class Factory( + private val serverTimeInMills: Long + ) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + return ServersCalendarDetailNoDataViewModel(serverTimeInMills) as T + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailReportAdapter.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailReportAdapter.kt similarity index 57% rename from app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailReportAdapter.kt rename to app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailReportAdapter.kt index 0d83ee7574..4105af851a 100644 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailReportAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailReportAdapter.kt @@ -3,17 +3,18 @@ package com.gh.gamecenter.gamedetail.fuli.kaifu import android.content.Context import android.view.ViewGroup import android.view.ViewGroup.MarginLayoutParams -import androidx.recyclerview.widget.RecyclerView import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.BaseRecyclerViewHolder import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.toColor import com.gh.gamecenter.databinding.DialogServersCalendearDetailReportItemBinding import com.gh.gamecenter.feature.entity.ServerCalendarEntity import com.lightgame.adapter.BaseRecyclerAdapter -class ServersDetailReportAdapter( +class ServersCalendarDetailReportAdapter( context: Context, - private val serverList: MutableList -) : BaseRecyclerAdapter(context) { + private val serverList: List +) : BaseRecyclerAdapter(context) { private val selectedPositions = mutableListOf() @@ -21,14 +22,14 @@ class ServersDetailReportAdapter( selectedPositions.contains(index) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServersDetailReportViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServersCalendarDetailReportViewHolder { val view = mLayoutInflater.inflate(R.layout.dialog_servers_calendear_detail_report_item, parent, false) - return ServersDetailReportViewHolder(DialogServersCalendearDetailReportItemBinding.bind(view)) + return ServersCalendarDetailReportViewHolder(DialogServersCalendearDetailReportItemBinding.bind(view)) } override fun getItemCount(): Int = serverList.size - override fun onBindViewHolder(holder: ServersDetailReportViewHolder, position: Int) { + override fun onBindViewHolder(holder: ServersCalendarDetailReportViewHolder, position: Int) { val serverCalendarEntity = serverList[position] holder.bindItem(serverCalendarEntity) @@ -50,4 +51,18 @@ class ServersDetailReportAdapter( notifyItemChanged(position) } } + + class ServersCalendarDetailReportViewHolder(val binding: DialogServersCalendearDetailReportItemBinding) : + BaseRecyclerViewHolder(binding.root) { + + fun bindItem(data: ServerCalendarEntity) { + binding.name.text = data.getNote() + binding.time.text = data.getFormatTime("HH:mm") + binding.remark.text = data.remark + + binding.name.setTextColor(R.color.text_title.toColor(binding.root.context)) + binding.time.setTextColor(R.color.text_title.toColor(binding.root.context)) + binding.remark.setTextColor(R.color.text_title.toColor(binding.root.context)) + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailViewModel.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailViewModel.kt new file mode 100644 index 0000000000..3097073d5a --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailViewModel.kt @@ -0,0 +1,38 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import androidx.lifecycle.* +import com.gh.gamecenter.entity.CalendarEntity + +class ServersCalendarDetailViewModel( + source: LiveData>, + val year: Int, + val month: Int, + val day: Int, + val showRemind: Boolean +) : ViewModel() { + + var calendarEntityLiveData = Transformations.switchMap(source) { + Transformations.distinctUntilChanged( + MutableLiveData( + it.find { calendar -> + calendar.year == year + && calendar.month == month + && calendar.day == day + } + ) + ) + } + + class Factory( + private val source: LiveData>, + private val year: Int, + private val month: Int, + private val day: Int, + private val showRemind: Boolean + ) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + return ServersCalendarDetailViewModel(source, year, month, day, showRemind) as T + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailsRemindDialog.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailsRemindDialog.kt new file mode 100644 index 0000000000..66bf93b113 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailsRemindDialog.kt @@ -0,0 +1,320 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Dialog +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import com.gh.common.constant.Config +import com.gh.common.util.CheckLoginUtils +import com.gh.common.util.NewLogUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.common.utils.SensorsBridge +import com.gh.gamecenter.common.utils.WXAPIProxyFactory +import com.gh.gamecenter.common.utils.WXAPIProxyFactory.WXAPIProxy +import com.gh.gamecenter.common.utils.WXAPIProxyFactory.WXHandler +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.setDrawableEnd +import com.gh.gamecenter.common.utils.viewModelProvider +import com.gh.gamecenter.common.view.DrawableView +import com.gh.gamecenter.core.utils.SPUtils +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.databinding.DialogServersDetailRemindBinding +import com.gh.gamecenter.login.user.UserManager +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.tencent.mm.opensdk.modelbase.BaseResp +import com.tencent.mm.opensdk.modelbiz.SubscribeMessage +import com.tencent.mm.opensdk.openapi.IWXAPI +import com.tencent.mm.opensdk.openapi.WXAPIFactory +import java.text.SimpleDateFormat +import java.util.* + +/** + * 游戏开服-开服表详情-提醒详情弹窗 + */ +class ServersCalendarDetailsRemindDialog : BottomSheetDialogFragment() { + + private lateinit var viewBinding: DialogServersDetailRemindBinding + + private lateinit var viewModel: ServersCalendarDetailsRemindViewModel + + private lateinit var wxApi: WXAPIProxy + + private val parentViewModel by viewModels({ + parentFragment ?: this + }) + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) + + private val activityViewModel by activityViewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.DialogWindowTransparent) + wxApi = WXAPIProxyFactory.createWXAPI( + requireContext(), + Config.WECHAT_APPID, + object: WXHandler { + private val reserved = System.currentTimeMillis().toString() + + override fun handleOnReq(req: SubscribeMessage.Req) { + req.reserved = reserved + } + + override fun handleOnResp(resp: BaseResp): SubscribeMessage.Resp? { + if (resp is SubscribeMessage.Resp && reserved == resp.reserved) { + return resp + } + return null + } + } + ) + viewModel = viewModelProvider( + ServersCalendarDetailsRemindViewModel.Factory( + arguments?.getString(EntranceConsts.KEY_SERVER_CALENDAR_ID, "") ?: "" + ) + ) + viewModel.initCalendarLiveData(parentViewModel.calendarEntityLiveData) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + window?.setWindowAnimations(R.style.community_publication_animation) + window?.setGravity(Gravity.BOTTOM) + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return DialogServersDetailRemindBinding.inflate(inflater, container, false).also { + viewBinding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewBinding.appRemindCheckIv.setImageDrawable(DrawableView.getCheckSelectorDrawable(requireContext())) + viewBinding.wechatRemindCheckIv.setImageDrawable(DrawableView.getCheckSelectorDrawable(requireContext())) + + viewModel.serverCalendarEntityLiveData.observe(viewLifecycleOwner) { entity -> + entity?.let { + val serverName = if (it.getNote().isNullOrEmpty()) it.remark else it.getNote() + + viewModel.initData(it) + + viewBinding.serverName.text = serverName + viewBinding.serverTime.text = dateFormat.format(viewModel.selectedNotifyTime) + viewBinding.serverRemindTime.text = viewModel.selectedAdvancedTime.text + + viewBinding.appRemindCheckIv.isChecked = viewModel.appRemind + viewBinding.wechatRemindCheckIv.isChecked = viewModel.wechatRemind + + val notifySetting = it.notifySetting + if (notifySetting != null) { + viewBinding.serverTime.isEnabled = false + viewBinding.serverTime.setPadding(12F.dip2px(), 0, 0, 0) + viewBinding.serverTime.setDrawableEnd(null) + viewBinding.serverTime.background = + ContextCompat.getDrawable(requireContext(), R.drawable.bg_server_detail_remind_time_disabled) + viewBinding.serverTime.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.text_subtitleDesc + ) + ) + + viewBinding.serverRemindTime.isEnabled = false + viewBinding.serverRemindTime.setPadding(12F.dip2px(), 0, 0, 0) + viewBinding.serverRemindTime.setDrawableEnd(null) + viewBinding.serverRemindTime.background = + ContextCompat.getDrawable(requireContext(), R.drawable.bg_server_detail_remind_time_disabled) + viewBinding.serverRemindTime.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.text_subtitleDesc + ) + ) + + viewBinding.appRemindCheckIv.alpha = 0.4F + viewBinding.appRemind.isEnabled = false + + viewBinding.wechatRemindCheckIv.alpha = 0.4F + viewBinding.wechatRemind.isEnabled = false + + viewBinding.addOrCancelRemind.text = getString(R.string.servers_detail_cancel_remind) + viewBinding.addOrCancelRemind.background = + ContextCompat.getDrawable(requireContext(), R.drawable.bg_servers_detail_cancel_remind) + viewBinding.addOrCancelRemind.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.text_subtitle + ) + ) + viewBinding.addOrCancelRemind.setOnClickListener { + CheckLoginUtils.checkLogin(view.context, "游戏详情-开服日历表-已知服详情") { + NewLogUtils.logLaunchServerReminderCancel( + gameId = activityViewModel.game.id, + gameName = activityViewModel.game.name ?: "", + reminderTime = viewModel.selectedNotifySeconds - viewModel.selectedAdvancedTime.value, + reminderType = "已知服", + serverName = serverName ?: "", + launchServerTime = viewModel.selectedNotifySeconds, + isApp = viewModel.appRemind, + isWechat = viewModel.wechatRemind + ) + SensorsBridge.trackLaunchServerReminderCancelClick( + gameId = activityViewModel.game.id, + gameName = activityViewModel.game.name ?: "", + reminderType = "已知服" + ) + viewModel.removeServerCalendarRemind(activityViewModel.game.id) + } + } + } else { + viewBinding.serverTime.isEnabled = true + viewBinding.serverTime.setPadding(12F.dip2px(), 0, 12F.dip2px(), 0) + viewBinding.serverTime.background = + ContextCompat.getDrawable(requireContext(), R.drawable.bg_server_detail_remind_time) + viewBinding.serverTime.setDrawableEnd(R.drawable.ic_arrow_gray) + viewBinding.serverTime.setTextColor(ContextCompat.getColor(requireContext(), R.color.text_subtitle)) + + viewBinding.serverRemindTime.isEnabled = true + viewBinding.serverRemindTime.setPadding(12F.dip2px(), 0, 12F.dip2px(), 0) + viewBinding.serverRemindTime.background = + ContextCompat.getDrawable(requireContext(), R.drawable.bg_server_detail_remind_time) + viewBinding.serverRemindTime.setDrawableEnd(R.drawable.ic_arrow_gray) + viewBinding.serverRemindTime.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.text_subtitle + ) + ) + + viewBinding.appRemindCheckIv.alpha = 1.0F + viewBinding.appRemind.isEnabled = true + + viewBinding.wechatRemindCheckIv.alpha = 1.0F + viewBinding.wechatRemind.isEnabled = true + + viewBinding.addOrCancelRemind.text = getString(R.string.servers_detail_add_remind) + viewBinding.addOrCancelRemind.setTextColor(ContextCompat.getColor(requireContext(), R.color.white)) + viewBinding.addOrCancelRemind.background = + ContextCompat.getDrawable(requireContext(), R.drawable.download_button_normal_style) + viewBinding.addOrCancelRemind.setOnClickListener { view -> + CheckLoginUtils.checkLogin(view.context, "游戏详情-开服日历表-已知服详情") { + if (!viewModel.wechatRemind && !viewModel.appRemind) { + ToastUtils.showToast(getString(R.string.servers_calendar_no_remind_checked_hint)) + return@checkLogin + } + + val selectedNotifyTime = viewModel.selectedNotifyTime + val selectedAdvancedTime = viewModel.selectedAdvancedTime.value + if (selectedNotifyTime - selectedAdvancedTime * 1000 < System.currentTimeMillis()) { + ToastUtils.showToast(getString(R.string.servers_calendar_remind_time_out_of_date)) + return@checkLogin + } + + NewLogUtils.logLaunchServerReminderAdd( + gameId = activityViewModel.game.id, + gameName = activityViewModel.game.name ?: "", + reminderTime = viewModel.selectedNotifySeconds - viewModel.selectedAdvancedTime.value, + reminderType = "已知服", + serverName = serverName ?: "", + launchServerTime = viewModel.selectedNotifySeconds, + isApp = viewModel.appRemind, + isWechat = viewModel.wechatRemind + ) + + SensorsBridge.trackLaunchServerReminderClick( + gameId = activityViewModel.game.id, + gameName = activityViewModel.game.name ?: "", + reminderType = "已知服" + ) + + if (viewModel.wechatRemind) {// 勾选微信订阅后需要微信授权一次性订阅消息 + viewModel.getWXSubscribeMsgConfig() + } else { + viewModel.addServerCalendarRemind(activityViewModel.game.id) + } + } + } + } + } + } + + viewModel.wxSubscribeMsgConfigLiveData.observe(viewLifecycleOwner) { + wxApi.sendReq( + SubscribeMessage.Req().apply { + scene = it.scene + templateID = it.templateId + reserved = it.reserved + } + ) + } + + viewModel.error.observe(viewLifecycleOwner) { + ToastUtils.showToast(getString(R.string.network_error_hint)) + } + + wxApi.liveData.observe(viewLifecycleOwner) { + if (it.action == "confirm") { + viewModel.addServerCalendarRemindWithWechat( + gameId = activityViewModel.game.id, + openId = it.openId, + templateId = it.templateID, + action = it.action, + reserved = UserManager.getInstance().userId, + scene = it.scene + ) + } + } + + viewBinding.close.setOnClickListener { + dismissAllowingStateLoss() + } + + viewBinding.serverTime.setOnClickListener { + val entity = viewModel.serverCalendarEntityLiveData.value ?: return@setOnClickListener + ServersCalendarTimeSettingDialog + .create(viewModel.selectedNotifyTime, entity.getTime() * 1000) { + viewModel.selectedNotifyTime = it + viewBinding.serverTime.text = dateFormat.format(it) + } + .showNow(childFragmentManager, null) + } + + viewBinding.serverRemindTime.setOnClickListener { + ServersCalendarRemindTimeSettingDialog().also { + it.setSelectCallback { time -> + viewModel.selectedAdvancedTime = time + viewBinding.serverRemindTime.text = time.text + } + it.showNow(childFragmentManager, null) + } + } + + viewBinding.appRemind.setOnClickListener { + viewBinding.appRemindCheckIv.isChecked = !viewBinding.appRemindCheckIv.isChecked + viewModel.appRemind = viewBinding.appRemindCheckIv.isChecked + SPUtils.setBoolean(Constants.SP_SERVERS_CALENDAR_BY_APP, viewBinding.appRemindCheckIv.isChecked) + } + + viewBinding.wechatRemind.setOnClickListener { + viewBinding.wechatRemindCheckIv.isChecked = !viewBinding.wechatRemindCheckIv.isChecked + viewModel.wechatRemind = viewBinding.wechatRemindCheckIv.isChecked + SPUtils.setBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, viewBinding.wechatRemindCheckIv.isChecked) + } + + viewModel.notifySettingChangeLiveData.observe(viewLifecycleOwner) { + activityViewModel.dispatchNotifySettingChange(it.first, it.second) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailsRemindViewModel.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailsRemindViewModel.kt new file mode 100644 index 0000000000..9fe53477f1 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarDetailsRemindViewModel.kt @@ -0,0 +1,160 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.annotation.SuppressLint +import androidx.lifecycle.* +import com.gh.gamecenter.common.constant.Constants +import com.gh.gamecenter.common.livedata.NonStickyMutableLiveData +import com.gh.gamecenter.common.utils.toRequestBody +import com.gh.gamecenter.core.utils.SPUtils +import com.gh.gamecenter.core.utils.UrlFilterUtils +import com.gh.gamecenter.entity.CalendarEntity +import com.gh.gamecenter.feature.entity.ServerCalendarEntity +import com.gh.gamecenter.feature.entity.ServerCalendarNotifySetting +import com.gh.gamecenter.feature.entity.WXSubscribeMsgConfig +import com.gh.gamecenter.retrofit.RetrofitManager +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class ServersCalendarDetailsRemindViewModel( + val id: String +) : ViewModel() { + + val error = NonStickyMutableLiveData() + + lateinit var serverCalendarEntityLiveData: LiveData + private set + + val notifySettingChangeLiveData = MutableLiveData>() + + val wxSubscribeMsgConfigLiveData = MutableLiveData() + + private var isDataInit = false + + var selectedNotifyTime: Long = 0L + + var selectedAdvancedTime: ServersCalendarAdvancedTime = ServersCalendarAdvancedTime.IN_TIME + + var appRemind: Boolean = false + + var wechatRemind: Boolean = false + + val selectedNotifySeconds: Int get() = (selectedNotifyTime / 1000).toInt() + + fun initCalendarLiveData(source: LiveData) { + serverCalendarEntityLiveData = Transformations.switchMap(source) { + Transformations.distinctUntilChanged( + MutableLiveData( + it?.server?.find { calendar -> + calendar.id == id + } + ) + ) + } + } + + fun initData(calendarEntity: ServerCalendarEntity) { + if (isDataInit) return + val notifySetting = calendarEntity.notifySetting + selectedAdvancedTime = ServersCalendarAdvancedTime.valueOf(notifySetting?.secondsAdvance) + selectedNotifyTime = (notifySetting?.notifyTime ?: calendarEntity.getTime()) * 1000 + appRemind = notifySetting?.byApp ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_APP, true) + wechatRemind = notifySetting?.byWechat ?: SPUtils.getBoolean(Constants.SP_SERVERS_CALENDAR_BY_WECHAT, true) + isDataInit = true + } + + @SuppressLint("CheckResult") + fun getServerCalenderRemind(gameId: String) { + RetrofitManager.getInstance().newApi + .getServerCalendarRemind(gameId, id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + notifySettingChangeLiveData.postValue(id to it) + }, { + notifySettingChangeLiveData.postValue(id to null) + }) + } + + @SuppressLint("CheckResult") + fun getWXSubscribeMsgConfig() { + RetrofitManager.getInstance().newApi + .getWxSubscribeMsgConfig(UrlFilterUtils.getFilterQuery("type", "server_open")) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + wxSubscribeMsgConfigLiveData.postValue(it) + }, { + error.postValue(it) + }) + } + + @SuppressLint("CheckResult") + fun addServerCalendarRemind(gameId: String) { + val requestBody = mapOf( + "notify_time" to selectedNotifySeconds, + "seconds_advance" to selectedAdvancedTime.value, + "by_app" to appRemind, + "by_wechat" to wechatRemind + ).toRequestBody() + + RetrofitManager.getInstance().newApi + .addServerCalendarRemind(gameId, id, requestBody) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + getServerCalenderRemind(gameId) + }, { + error.postValue(it) + }) + } + + @SuppressLint("CheckResult") + fun addServerCalendarRemindWithWechat( + gameId: String, + openId: String?, + templateId: String?, + action: String, + reserved: String, + scene: Int, + ) { + val requestBody = mapOf( + "openid" to openId, + "template_id" to templateId, + "action" to action, + "scene" to scene, + "user_id" to reserved + ).toRequestBody() + + RetrofitManager.getInstance().newApi + .postWxSubscribeMsgCallback(UrlFilterUtils.getFilterQuery("type", "server_open"), requestBody) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + addServerCalendarRemind(gameId) + }, { + error.postValue(it) + }) + } + + @SuppressLint("CheckResult") + fun removeServerCalendarRemind(gameId: String) { + RetrofitManager.getInstance().newApi + .removeServerCalendarRemind(gameId, id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + getServerCalenderRemind(gameId) + }, { + error.postValue(it) + }) + } + + class Factory( + private val id: String + ) : ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T { + return ServersCalendarDetailsRemindViewModel(id) as T + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListAdapter.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListAdapter.kt new file mode 100644 index 0000000000..bed32bfb09 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListAdapter.kt @@ -0,0 +1,176 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.content.Context +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.gh.common.util.DirectUtils +import com.gh.common.util.NewLogUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.baselist.ListAdapter +import com.gh.gamecenter.common.baselist.LoadType +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.common.viewholder.FooterViewHolder +import com.gh.gamecenter.databinding.ItemGameServerTestTimeBinding +import com.gh.gamecenter.databinding.ItemServerCalendarServerBinding +import com.gh.gamecenter.databinding.ItemServersCalendarBinding +import com.gh.gamecenter.feature.game.GameItemViewHolder +import splitties.views.backgroundColor + +class ServersCalendarListAdapter( + context: Context, + private val viewModel: ServersCalendarListViewModel +) : ListAdapter(context) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return when (viewType) { + ITEM_TYPE_FOOTER -> FooterViewHolder(mLayoutInflater.inflate(R.layout.refresh_footerview, parent, false)) + ITEM_TYPE_DATA_ITEM -> ServersCalendarViewHolder(parent.toBinding()) + else -> ServersCalendarTimeViewHolder(parent.toBinding()) + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + when (holder) { + is ServersCalendarViewHolder -> { + val game = mEntityList[position].game ?: return + + holder.binding.root.backgroundColor = ContextCompat.getColor(mContext, R.color.background_white) + + holder.binding.icon.displayGameIcon(game.toGameEntity()) + holder.binding.gameName.text = game.name + holder.binding.gameName.setTextColor(ContextCompat.getColor(mContext, R.color.text_title)) + + // 副标题 + GameItemViewHolder.initGameSubtitleAndAdLabel( + game.toGameEntity(), + holder.binding.subtitle + ) + + // 如果开服表总数超过3个,显示开服表总数 + if (game.serversTotal > MAX_DISPLAY_SERVER_LIST_SIZE) { + holder.binding.serverCount.visibility = View.VISIBLE + holder.binding.serverCount.text = + holder.binding.root.context.getString( + R.string.servers_calendar_list_server_count, + game.serversTotal + ) + } else { + holder.binding.serverCount.visibility = View.GONE + } + + // 最多显示3条开服列表数据 + val serverList = if (game.servers.size > MAX_DISPLAY_SERVER_LIST_SIZE) { + game.servers.subList(0, MAX_DISPLAY_SERVER_LIST_SIZE) + } else { + game.servers + } + holder.binding.servers.removeAllViews() + holder.binding.serversContainer.background = ContextCompat.getDrawable( + mContext, + R.drawable.bg_shape_f8_radius_8 + ) + serverList.forEachIndexed { index, server -> + ItemServerCalendarServerBinding.inflate( + mLayoutInflater, + holder.binding.servers, + true + ).apply { + // 优先展示服务器名字,若服务器名字为空则展示备注 + val note = server.getNote() + val remark = server.remark + val name = if (note.isNullOrEmpty()) remark else note + this.name.text = name + this.time.text = server.getFormatTime("HH:mm") + root.layoutParams = (root.layoutParams as ViewGroup.MarginLayoutParams).apply { + bottomMargin = if (index < serverList.size - 1) 8F.dip2px() else 0 + } + } + } + + holder.binding.root.setOnClickListener { + DirectUtils.directToGameServerCalendar( + context = it.context, + gameId = game.id, + kaifuTime = game.startMidnight + ) + NewLogUtils.logLaunchServerSubscribeClick( + gameId = game.id, + gameName = game.name, + launchServerTime = game.startMidnight.toInt() + ) + } + } + + is ServersCalendarTimeViewHolder -> { + holder.binding.root.backgroundColor = ContextCompat.getColor(mContext, R.color.background_white) + holder.binding.timeTv.setTextColor(ContextCompat.getColor(mContext, R.color.text_subtitle)) + holder.binding.timeTv.text = + ServersCalendarListFragment.formatTime(mContext, (mEntityList[position].time ?: 0) * 1000) + } + + is FooterViewHolder -> initFooterViewHolder(holder, mIsLoading, mIsNetworkError, mIsOver) + } + } + + fun initFooterViewHolder( + viewHolder: FooterViewHolder, + isLoading: Boolean, + isNetworkError: Boolean, + isOver: Boolean + ) { + viewHolder.run { + if (isNetworkError) { + loading.visibility = View.GONE + hint.setText(R.string.loading_failed_retry) + itemView.isClickable = true + itemView.setOnClickListener { viewModel.load(LoadType.RETRY) } + } else if (isOver) { + loading.visibility = View.GONE + hint.setText(R.string.load_over_hint) + itemView.isClickable = false + itemView.setOnClickListener(null) + } else if (isLoading) { + loading.visibility = View.VISIBLE + hint.setText(R.string.loading) + itemView.isClickable = false + itemView.setOnClickListener(null) + } else { + loading.visibility = View.GONE + hint.setText(R.string.loading_more_hint) + itemView.isClickable = false + itemView.setOnClickListener(null) + } + } + } + + override fun getItemViewType(position: Int): Int { + return if (mEntityList[position].footer) { + ITEM_TYPE_FOOTER + } else if(mEntityList[position].time != null) { + ITEM_TYPE_DATE_HEADER + } else { + ITEM_TYPE_DATA_ITEM + } + } + + override fun getItemCount(): Int = mEntityList.size + + class ServersCalendarViewHolder(val binding: ItemServersCalendarBinding) : ViewHolder(binding.root) + + class ServersCalendarTimeViewHolder(var binding: ItemGameServerTestTimeBinding) : ViewHolder(binding.root) + + companion object { + + private const val ITEM_TYPE_FOOTER = 100 + + private const val ITEM_TYPE_DATE_HEADER = 200 + + private const val ITEM_TYPE_DATA_ITEM = 300 + + private const val MAX_DISPLAY_SERVER_LIST_SIZE = 3 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListFragment.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListFragment.kt new file mode 100644 index 0000000000..c25064135f --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListFragment.kt @@ -0,0 +1,143 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.gh.common.util.DirectUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.baselist.LazyListFragment +import com.gh.gamecenter.common.baselist.ListAdapter +import com.gh.gamecenter.common.utils.viewModelProvider +import com.gh.gamecenter.databinding.FragmentServersCalendarListBinding +import java.text.SimpleDateFormat +import java.util.* + +/** + * 开服订阅-开服表 + */ +class ServersCalendarListFragment : + LazyListFragment() { + + private var adapter: ServersCalendarListAdapter? = null + + private lateinit var viewBinding: FragmentServersCalendarListBinding + + override fun getRealLayoutId(): Int { + return R.layout.fragment_servers_calendar_list + } + + override fun onRealLayoutInflated(inflatedView: View) { + super.onRealLayoutInflated(inflatedView) + viewBinding = FragmentServersCalendarListBinding.bind(inflatedView) + } + + override fun onRefresh() { + super.onRefresh() + viewBinding.topDateContainer.root.visibility = View.GONE + } + + override fun onLoadError() { + super.onLoadError() + viewBinding.topDateContainer.root.visibility = View.GONE + } + + override fun onLoadEmpty() { + super.onLoadEmpty() + viewBinding.topDateContainer.root.visibility = View.GONE + } + + override fun provideListViewModel(): ServersCalendarListViewModel { + return viewModelProvider() + } + + override fun provideListAdapter(): ListAdapter<*> { + return adapter ?: ServersCalendarListAdapter( + requireContext(), + mListViewModel + ).apply { + adapter = this + } + } + + override fun getItemDecoration(): RecyclerView.ItemDecoration? = null + + override fun onFragmentFirstVisible() { + super.onFragmentFirstVisible() + + viewBinding.subscribedGameManagementLayout.setOnClickListener { + DirectUtils.directToServersSubscribedGameList(it.context) + } + + mListRv.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val layoutManager = recyclerView.layoutManager + if (layoutManager is LinearLayoutManager && !mListViewModel.obsListData.value.isNullOrEmpty()) { + val firstVisiblePosition = layoutManager.findFirstVisibleItemPosition() + + // 悬挂的文案 + viewBinding.topDateContainer.root.visibility = View.VISIBLE + viewBinding.topDateContainer.timeTv.text = + formatTime(recyclerView.context, mListViewModel.getTimeByPosition(firstVisiblePosition) * 1000) + + // 悬挂界面移动 + val lp = viewBinding.topDateContainer.root.layoutParams as FrameLayout.LayoutParams + if (firstVisiblePosition != 0 + && mListViewModel?.getItemAtPosition(firstVisiblePosition + 1)?.time != null + ) { + val bottom = layoutManager.findViewByPosition(firstVisiblePosition)?.bottom ?: 0 + if (bottom <= viewBinding.topDateContainer.root.height) { + lp.topMargin = bottom - viewBinding.topDateContainer.root.height + } else { + lp.topMargin = 0 + } + } else { + lp.topMargin = 0 + } + viewBinding.topDateContainer.root.layoutParams = lp + } + } + + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {} + }) + } + + companion object { + + /** + * 将时间戳转换成开服表时间 + * 开服表时间显示规则: + * 明天:目标日期=当天的下一天日期 + * 今天:目标日期=当天日期 + * 月日:目标日期在当前自然年以内 + * 年月日:目标日期在当前自然年以外 + */ + fun formatTime(context: Context, time: Long): String { + val serverCalendar = Calendar.getInstance().apply { + timeInMillis = time + } + val todayCalendar = Calendar.getInstance() + val tomorrowCalendar = Calendar.getInstance().apply { + add(Calendar.DATE, 1) + } + + return if (todayCalendar.get(Calendar.YEAR) == serverCalendar.get(Calendar.YEAR) + && todayCalendar.get(Calendar.MONTH) == serverCalendar.get(Calendar.MONTH) + && todayCalendar.get(Calendar.DATE) == serverCalendar.get(Calendar.DATE) + ) { + context.getString(R.string.servers_calendar_list_date_today) + } else if (tomorrowCalendar.get(Calendar.YEAR) == serverCalendar.get(Calendar.YEAR) + && tomorrowCalendar.get(Calendar.MONTH) == serverCalendar.get(Calendar.MONTH) + && tomorrowCalendar.get(Calendar.DATE) == serverCalendar.get(Calendar.DATE) + ) { + context.getString(R.string.servers_calendar_list_date_tomorrow) + } else if (todayCalendar.get(Calendar.YEAR) == serverCalendar.get(Calendar.YEAR)) { + SimpleDateFormat("MM月dd日", Locale.getDefault()).format(time) + } else { + SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault()).format(time) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListViewModel.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListViewModel.kt new file mode 100644 index 0000000000..1c60e4d677 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarListViewModel.kt @@ -0,0 +1,77 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Application +import com.gh.gamecenter.common.baselist.ListViewModel +import com.gh.gamecenter.entity.HaloAddonEntity +import com.gh.gamecenter.feature.entity.ServerCalendarFormEntity +import com.gh.gamecenter.feature.entity.ServerCalendarGame +import com.gh.gamecenter.retrofit.RetrofitManager +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import io.reactivex.Observable +import io.reactivex.Single + +class ServersCalendarListViewModel(application: Application) : + ListViewModel( + application + ) { + + override fun provideDataObservable(page: Int): Observable>? { + return null + } + + override fun provideDataSingle(page: Int): Single> { + return RetrofitManager.getInstance().newApi + .getServerCalendarList(page, 20) + .map { list -> + val dataList = mutableListOf() + for (formEntity in list) { + dataList.add(ItemData(time = formEntity.startMidnight)) + dataList.addAll(formEntity.games.map { game -> + ItemData(game = game.apply { + startMidnight = formEntity.startMidnight + }) + }) + } + dataList + } + } + + fun getTimeByPosition(position: Int): Long { + var time = 0L + val dataList = mResultLiveData.value + + dataList?.let { + for ((index, value) in dataList.withIndex()) { + if (value.time != null) { + time = value.time + } + if (index == position) { + return time + } + } + } + return time + } + + fun getItemAtPosition(position: Int): ItemData? { + if (position < (mResultLiveData?.value?.size ?: 0)) { + return mResultLiveData.value?.get(position) + } + return null + } + + override fun mergeResultLiveData() { + mResultLiveData.addSource(mListLiveData) { list -> + mResultLiveData.postValue( + mutableListOf() + .also { + it.addAll(list) + it.add(ItemData(footer = true)) + } + ) + } + } + + data class ItemData(val game: ServerCalendarGame? = null, val time: Long? = null, val footer: Boolean = false) +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarManagementActivity.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarManagementActivity.kt new file mode 100644 index 0000000000..ead3a6852a --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarManagementActivity.kt @@ -0,0 +1,40 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.PersistableBundle +import androidx.fragment.app.Fragment +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.activity.BaseActivity_TabLayout +import com.gh.gamecenter.common.utils.updateStatusBarColor + +class ServersCalendarManagementActivity : BaseActivity_TabLayout() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setNavigationTitle(R.string.servers_calendar_subscription_title) + updateStatusBarColor(R.color.background_white, R.color.background_white) + } + + override fun initFragmentList(fragments: MutableList?) { + fragments?.add(ServersCalendarListFragment()) + fragments?.add(ServersCalendarRemindListFragment()) + } + + override fun initTabTitleList(tabTitleList: MutableList?) { + tabTitleList?.add(getString(R.string.servers_calendar_list_tab)) + tabTitleList?.add(getString(R.string.servers_calendar_remind_list_tab)) + } + + override fun onDarkModeChanged() { + super.onDarkModeChanged() + updateStatusBarColor(R.color.background_white, R.color.background_white) + } + + companion object { + fun getIntent(context: Context): Intent { + return Intent(context, ServersCalendarManagementActivity::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarMoreDialog.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarMoreDialog.kt new file mode 100644 index 0000000000..8c92788cbf --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarMoreDialog.kt @@ -0,0 +1,63 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.activityViewModels +import com.gh.common.util.NewLogUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.utils.SensorsBridge +import com.gh.gamecenter.databinding.DialogServersCalendarMoreBinding +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +/** + * 开服日历表-更多菜单 + */ +class ServersCalendarMoreDialog : BottomSheetDialogFragment() { + + private lateinit var viewBinding: DialogServersCalendarMoreBinding + + private val viewModel by activityViewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.DialogWindowTransparent) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + window?.setWindowAnimations(R.style.community_publication_animation) + window?.setGravity(Gravity.BOTTOM) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewBinding.unsubscribe.setOnClickListener { + viewModel.unsubscribeServer() + NewLogUtils.logLaunchServerSubscribeCancel( + gameId = viewModel.game.id, + gameName = viewModel.game.name ?: "" + ) + SensorsBridge.trackLaunchServerSubscribeCancelClick( + gameId = viewModel.game.id, + gameName = viewModel.game.name ?: "" + ) + dismissAllowingStateLoss() + } + + viewBinding.cancel.setOnClickListener { + dismissAllowingStateLoss() + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return DialogServersCalendarMoreBinding.inflate(inflater, container, false).also { + viewBinding = it + }.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListAdapter.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListAdapter.kt new file mode 100644 index 0000000000..80569e100a --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListAdapter.kt @@ -0,0 +1,186 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.gh.common.util.DirectUtils +import com.gh.common.util.NewLogUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.baselist.ListAdapter +import com.gh.gamecenter.common.baselist.LoadType +import com.gh.gamecenter.common.utils.toBinding +import com.gh.gamecenter.common.viewholder.FooterViewHolder +import com.gh.gamecenter.databinding.ItemServersCalendarRemindBinding +import com.gh.gamecenter.feature.game.GameItemViewHolder +import java.text.SimpleDateFormat +import java.util.* + +class ServersCalendarRemindListAdapter( + context: Context, + private val viewModel: ServersCalendarRemindListViewModel +) : ListAdapter(context) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return when (viewType) { + ITEM_TYPE_FOOTER -> FooterViewHolder(mLayoutInflater.inflate(R.layout.refresh_footerview, parent, false)) + else -> ServersCalendarRemindViewHolder(parent.toBinding()) + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (holder is ServersCalendarRemindViewHolder) { + val game = mEntityList[position].game ?: return + + holder.binding.icon.displayGameIcon(game.toGameEntity()) + holder.binding.gameName.text = game.name + + // 副标题 + GameItemViewHolder.initGameSubtitleAndAdLabel( + game.toGameEntity(), + holder.binding.subtitle + ) + + val context = holder.binding.root.context + + // 已知服: 展示设置开服时间+服务器名字/备注 + // 未知服: 展示关注开服日期 + val server = game.servers.firstOrNull() ?: return + val notifySetting = server.notifySetting ?: return + // 提醒状态 + val notifyStatus = game.notifyStatus ?: return + if ((server.getNote() + .isNullOrEmpty() && server.remark.isEmpty()) || notifyStatus == NOTIFY_STATUS_HAS_NEW || notifyStatus == NOTIFY_STATUS_EXPIRED + ) {// 未知服或未知服相关状态,仅展示日期 + val format = SimpleDateFormat("MM月dd日", Locale.getDefault()) + holder.binding.time.text = format.format(notifySetting.notifyTime * 1000) + } else {// 已知服 + val format = SimpleDateFormat("MM月dd日 HH:mm", Locale.getDefault()) + // 优先展示服务器名字,若服务器名字为空则展示备注 + val note = server.getNote() + val remark = server.remark + val name = if (note.isNullOrEmpty()) remark else note + holder.binding.time.text = context.getString( + R.string.servers_calendar_remind_list_server_name, + format.format(notifySetting.notifyTime * 1000), + name + ) + } + + // 设置提醒状态文字颜色 + val statusTextColorId = if (notifyStatus == NOTIFY_STATUS_TODO || notifyStatus == NOTIFY_STATUS_COMING) { + R.color.theme_font + } else { + R.color.text_subtitleDesc + } + holder.binding.status.setTextColor(ContextCompat.getColor(context, statusTextColorId)) + + // 设置提醒状态文案 + val statusTextId = when (notifyStatus) { + NOTIFY_STATUS_TODO -> R.string.servers_calendar_remind_status_todo + NOTIFY_STATUS_COMING -> R.string.servers_calendar_remind_status_coming + NOTIFY_STATUS_OPENED -> R.string.servers_calendar_remind_status_opened + NOTIFY_STATUS_HAS_NEW -> R.string.servers_calendar_remind_status_has_new + else -> R.string.servers_calendar_remind_status_expired + } + holder.binding.status.text = context.getString(statusTextId) + + holder.binding.root.setOnClickListener { + DirectUtils.directToGameServerCalendar( + context = it.context, + gameId = game.id, + kaifuTime = server.getTime() + ) + NewLogUtils.logLaunchServerReminderClick( + gameId = game.id, + gameName = game.name, + reminderTime = (notifySetting.notifyTime - notifySetting.secondsAdvance).toInt(), + status = notifyStatus + ) + } + + } else if (holder is FooterViewHolder) { + initFooterViewHolder(holder, mIsLoading, mIsNetworkError, mIsOver) + } + } + + fun initFooterViewHolder( + viewHolder: FooterViewHolder, + isLoading: Boolean, + isNetworkError: Boolean, + isOver: Boolean + ) { + viewHolder.run { + if (isNetworkError) { + loading.visibility = View.GONE + hint.setText(R.string.loading_failed_retry) + itemView.isClickable = true + itemView.setOnClickListener { viewModel.load(LoadType.RETRY) } + } else if (isOver) { + loading.visibility = View.GONE + hint.setText(R.string.load_over_hint) + itemView.isClickable = false + itemView.setOnClickListener(null) + } else if (isLoading) { + loading.visibility = View.VISIBLE + hint.setText(R.string.loading) + itemView.isClickable = false + itemView.setOnClickListener(null) + } else { + loading.visibility = View.GONE + hint.setText(R.string.loading_more_hint) + itemView.isClickable = false + itemView.setOnClickListener(null) + } + } + } + + override fun getItemViewType(position: Int): Int { + return if (mEntityList[position].footer) { + ITEM_TYPE_FOOTER + } else { + ITEM_TYPE_DATA + } + } + + override fun getItemCount(): Int = mEntityList.size + + class ServersCalendarRemindViewHolder(val binding: ItemServersCalendarRemindBinding) : ViewHolder(binding.root) + + companion object { + + /** + * 待提醒 + */ + private const val NOTIFY_STATUS_TODO = "todo" + + /** + * 即将开服 + */ + private const val NOTIFY_STATUS_COMING = "coming" + + /** + * 已开服 + */ + private const val NOTIFY_STATUS_OPENED = "opened" + + /** + * 有新服 + */ + private const val NOTIFY_STATUS_HAS_NEW = "has_new" + + /** + * 已过期 + */ + private const val NOTIFY_STATUS_EXPIRED = "expired" + + /** + * 未知服 + */ + private const val SERVER_TYPE_UNKNOWN = "未知服" + + private const val ITEM_TYPE_FOOTER = 100 + private const val ITEM_TYPE_DATA = 200 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListFragment.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListFragment.kt new file mode 100644 index 0000000000..01a53c12ff --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListFragment.kt @@ -0,0 +1,37 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.gh.gamecenter.R +import com.gh.gamecenter.common.baselist.LazyListFragment +import com.gh.gamecenter.common.baselist.ListAdapter +import com.gh.gamecenter.common.utils.viewModelProvider +import com.gh.gamecenter.feature.entity.ServerCalendarGame + +/** + * 开服订阅-开服提醒 + */ +class ServersCalendarRemindListFragment : LazyListFragment() { + + private var adapter: ServersCalendarRemindListAdapter? = null + + override fun provideListAdapter(): ListAdapter<*> { + return adapter ?: ServersCalendarRemindListAdapter( + requireContext(), + mListViewModel + ).apply { + adapter = this + } + } + + override fun onFragmentFirstVisible() { + super.onFragmentFirstVisible() + mCachedView.background = ContextCompat.getDrawable(requireContext(), R.color.background_white) + } + + override fun getItemDecoration(): RecyclerView.ItemDecoration? = null + + override fun provideListViewModel(): ServersCalendarRemindListViewModel { + return viewModelProvider() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListViewModel.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListViewModel.kt new file mode 100644 index 0000000000..7eae72147f --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindListViewModel.kt @@ -0,0 +1,44 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Application +import com.gh.gamecenter.common.baselist.ListViewModel +import com.gh.gamecenter.feature.entity.ServerCalendarGame +import com.gh.gamecenter.retrofit.RetrofitManager +import io.reactivex.Observable +import io.reactivex.Single + +class ServersCalendarRemindListViewModel(application: Application) : + ListViewModel(application) { + + override fun provideDataObservable(page: Int): Observable>? { + return null + } + + override fun provideDataSingle(page: Int): Single> { + return RetrofitManager.getInstance().newApi + .getServerCalendarRemindList(page, 20) + .map { list -> + val dataList = mutableListOf() + list.forEach { game -> + if (game.servers.isNotEmpty()) { + dataList.add(ItemData(game = game)) + } + } + dataList + } + } + + override fun mergeResultLiveData() { + mResultLiveData.addSource(mListLiveData) { list -> + mResultLiveData.postValue( + mutableListOf() + .also { + it.addAll(list) + it.add(ItemData(footer = true)) + } + ) + } + } + + data class ItemData(val game: ServerCalendarGame? = null, val footer: Boolean = false) +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindTimeSettingAdapter.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindTimeSettingAdapter.kt new file mode 100644 index 0000000000..fca221b4b5 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindTimeSettingAdapter.kt @@ -0,0 +1,32 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.content.Context +import android.view.ViewGroup +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.BaseRecyclerViewHolder +import com.gh.gamecenter.databinding.ItemServersCalendarRemindTimeSettingBinding +import com.lightgame.adapter.BaseRecyclerAdapter + +class ServersCalendarRemindTimeSettingAdapter( + context: Context, + private val itemCallback: (ServersCalendarAdvancedTime) -> Unit +) : BaseRecyclerAdapter(context) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServersCalendarRemindTimeSettingViewHolder { + val view = mLayoutInflater.inflate(R.layout.item_servers_calendar_remind_time_setting, parent, false) + return ServersCalendarRemindTimeSettingViewHolder(ItemServersCalendarRemindTimeSettingBinding.bind(view)) + } + + override fun getItemCount(): Int = ServersCalendarAdvancedTime.values().size + + override fun onBindViewHolder(holder: ServersCalendarRemindTimeSettingViewHolder, position: Int) { + val data = ServersCalendarAdvancedTime.values()[position] + holder.binding.nameTv.text = data.text + holder.binding.root.setOnClickListener { + itemCallback.invoke(data) + } + } + + class ServersCalendarRemindTimeSettingViewHolder(val binding: ItemServersCalendarRemindTimeSettingBinding) : + BaseRecyclerViewHolder(binding.root) +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindTimeSettingDialog.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindTimeSettingDialog.kt new file mode 100644 index 0000000000..da6d0b38be --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarRemindTimeSettingDialog.kt @@ -0,0 +1,59 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import com.gh.gamecenter.R +import com.gh.gamecenter.common.view.VerticalItemDecoration +import com.gh.gamecenter.databinding.DialogServersCalendarRemindTimeSettingBinding +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +/** + * 设置提醒时间弹窗 + */ +class ServersCalendarRemindTimeSettingDialog : BottomSheetDialogFragment() { + + private lateinit var viewBinding: DialogServersCalendarRemindTimeSettingBinding + + private var selectCallback: ((ServersCalendarAdvancedTime) -> Unit)? = null + + fun setSelectCallback(callback: ((ServersCalendarAdvancedTime) -> Unit)?) { + this.selectCallback = callback + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.DialogWindowTransparent) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + window?.setWindowAnimations(R.style.community_publication_animation) + window?.setGravity(Gravity.BOTTOM) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewBinding.cancel.setOnClickListener { + dismissAllowingStateLoss() + } + + viewBinding.rv.adapter = ServersCalendarRemindTimeSettingAdapter(requireContext()) { + this.selectCallback?.invoke(it) + dismissAllowingStateLoss() + } + viewBinding.rv.layoutManager = LinearLayoutManager(context) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return DialogServersCalendarRemindTimeSettingBinding.inflate(inflater, container, false).also { + viewBinding = it + }.root + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarSubscriptionSuccessDialog.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarSubscriptionSuccessDialog.kt new file mode 100644 index 0000000000..e8340576dd --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarSubscriptionSuccessDialog.kt @@ -0,0 +1,43 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import com.gh.gamecenter.R +import com.gh.gamecenter.common.databinding.DialogAlertDefaultBinding +import com.gh.gamecenter.common.utils.setDrawableStart +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +/** + * 游戏订阅成功-已绑定微信弹窗 + */ +class ServersCalendarSubscriptionSuccessDialog : DialogFragment() { + + private lateinit var viewBinding: DialogAlertDefaultBinding + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(BottomSheetDialogFragment.STYLE_NORMAL, R.style.DialogWindowTransparent) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return DialogAlertDefaultBinding.inflate(inflater, container, false).also { + viewBinding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewBinding.titleTv.text = getString(R.string.servers_calendar_subscription_dialog_title) + viewBinding.contentTv.text = getString(R.string.servers_calendar_subscription_dialog_content) + viewBinding.confirmTv.text = getString(R.string.servers_calendar_subscription_dialog_confirm) + viewBinding.centerDivider.visibility = View.GONE + viewBinding.cancelTv.visibility = View.GONE + viewBinding.titleTv.setDrawableStart(R.drawable.ic_reserve_success, null, null) + viewBinding.confirmTv.setOnClickListener { + dismissAllowingStateLoss() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarTimeSettingDialog.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarTimeSettingDialog.kt new file mode 100644 index 0000000000..3873718708 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarTimeSettingDialog.kt @@ -0,0 +1,229 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.app.Dialog +import android.os.Bundle +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import com.bigkoo.pickerview.adapter.ArrayWheelAdapter +import com.contrarywind.adapter.WheelAdapter +import com.contrarywind.view.WheelView +import com.gh.gamecenter.R +import com.gh.gamecenter.common.constant.EntranceConsts +import com.gh.gamecenter.databinding.DialogServersCalendarTimeSettingBinding +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import java.text.SimpleDateFormat +import java.util.* + +/** + * 设置开服时间弹窗 + */ +class ServersCalendarTimeSettingDialog private constructor() : BottomSheetDialogFragment() { + + private lateinit var viewBinding: DialogServersCalendarTimeSettingBinding + + private lateinit var kaifuCalendar: Calendar + + private lateinit var selectKaifuCalendar: Calendar + + private val currentCalendar = Calendar.getInstance() + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd hh时 mm分", Locale.getDefault()) + + private var selectCallback: ((Long) -> Unit)? = null + + fun setSelectCallback(callback: ((Long) -> Unit)?) { + this.selectCallback = callback + } + + private fun dispatchSelectCallback() { + val date = viewBinding.date.adapter.getItem(viewBinding.date.currentItem) + val hour = viewBinding.hour.adapter.getItem(viewBinding.hour.currentItem) + val minute = viewBinding.minute.adapter.getItem(viewBinding.minute.currentItem) + dateFormat.parse("$date $hour $minute")?.let { + selectCallback?.invoke(it.time) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.DialogWindowTransparent) + selectKaifuCalendar = Calendar.getInstance().apply { + timeInMillis = arguments?.getLong(EntranceConsts.KEY_KAIFU_SELECT_TIME) ?: 0L + } + kaifuCalendar = Calendar.getInstance().apply { + timeInMillis = arguments?.getLong(EntranceConsts.KEY_KAIFU_TIME) ?: 0L + } + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + window?.setWindowAnimations(R.style.community_publication_animation) + window?.setGravity(Gravity.BOTTOM) + if (this is BottomSheetDialog) { + behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_DRAGGING) { + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) { + } + }) + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + return DialogServersCalendarTimeSettingBinding.inflate(inflater, container, false).also { + viewBinding = it + }.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewBinding.cancel.setOnClickListener { + dismissAllowingStateLoss() + } + + viewBinding.confirm.setOnClickListener { + dispatchSelectCallback() + dismissAllowingStateLoss() + } + + + val dateList = buildDates(currentCalendar, kaifuCalendar) + + initWheelView( + viewBinding.date, + ArrayWheelAdapter(dateList), + calculateCurrentDateItem(currentCalendar, selectKaifuCalendar), + false + ) + + initWheelView( + viewBinding.hour, + ArrayWheelAdapter(buildHours()), + selectKaifuCalendar.get(Calendar.HOUR_OF_DAY) + ) + + initWheelView( + viewBinding.minute, + ArrayWheelAdapter(buildMinutes()), + selectKaifuCalendar.get(Calendar.MINUTE) + ) + } + + companion object { + + fun create( + kaifuSelectTime: Long, + kaifuTime: Long, + selectCallback: (Long) -> Unit + ): ServersCalendarTimeSettingDialog = + ServersCalendarTimeSettingDialog().apply { + arguments = Bundle().apply { + putLong(EntranceConsts.KEY_KAIFU_SELECT_TIME, kaifuSelectTime) + putLong(EntranceConsts.KEY_KAIFU_TIME, kaifuTime) + } + setSelectCallback(selectCallback) + } + + private fun initWheelView( + wheelView: WheelView, + adapter: WheelAdapter<*>, + currentIndex: Int, + isLoop: Boolean = true + ) { + wheelView.javaClass.getDeclaredField("itemsVisible").apply { + isAccessible = true + set(wheelView, 7) + } + wheelView.setLabel("") + wheelView.setCyclic(isLoop) + wheelView.setGravity(Gravity.CENTER) + wheelView.adapter = adapter + wheelView.currentItem = currentIndex + wheelView.setTextSize(14F) + wheelView.setLineSpacingMultiplier(3F) + wheelView.setTextXOffset(0) + wheelView.setDividerColor(ContextCompat.getColor(wheelView.context, R.color.transparent)) + wheelView.setTextColorCenter(ContextCompat.getColor(wheelView.context, R.color.text_title)) + wheelView.setTextColorCenter(ContextCompat.getColor(wheelView.context, R.color.text_title)) + } + + private fun calculateCurrentDateItem( + currentCalendar: Calendar, + selectKaifuCalendar: Calendar + ): Int { + val selectKaifuTime = Calendar.getInstance().apply { + timeInMillis = selectKaifuCalendar.timeInMillis + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + val currentTime = Calendar.getInstance().apply { + timeInMillis = currentCalendar.timeInMillis + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + return ((selectKaifuTime - currentTime) / (24 * 60 * 60 * 1000)).toInt() + + } + + private fun buildDates(currentCalendar: Calendar, kaifuCalendar: Calendar): List { + val dateList = mutableListOf() + val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + val calendar = Calendar.getInstance().apply { + timeInMillis = currentCalendar.timeInMillis + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + + val kaifuTime = Calendar.getInstance().apply { + timeInMillis = kaifuCalendar.timeInMillis + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + var dateTime = calendar.timeInMillis + var dateOffset = (kaifuTime - dateTime) / (24 * 60 * 60 * 1000) + while (dateOffset >= 0) { + dateList.add(dateFormat.format(dateTime)) + dateTime = calendar.apply { + add(Calendar.DAY_OF_YEAR, 1) + }.timeInMillis + dateOffset = (kaifuTime - dateTime) / (24 * 60 * 60 * 1000) + } + return dateList + } + + private fun buildHours(): List { + val hourList = mutableListOf() + for (hour in 0..23) { + hourList.add("${hour}时") + } + return hourList + } + + private fun buildMinutes(): List { + val minuteList = mutableListOf() + for (minute in 0..59) { + minuteList.add("${minute}分") + } + return minuteList + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarViewModel.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarViewModel.kt index c3afdd7e37..8709437666 100644 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarViewModel.kt @@ -7,14 +7,12 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import com.gh.common.util.CheckLoginUtils -import com.gh.gamecenter.common.utils.throwExceptionInDebug -import com.gh.gamecenter.entity.* +import com.gh.gamecenter.common.livedata.NonStickyMutableLiveData import com.gh.gamecenter.common.retrofit.BiResponse +import com.gh.gamecenter.common.utils.throwExceptionInDebug import com.gh.gamecenter.common.utils.toRequestBody -import com.gh.gamecenter.feature.entity.GameDetailServer -import com.gh.gamecenter.feature.entity.GameEntity -import com.gh.gamecenter.feature.entity.MeEntity -import com.gh.gamecenter.feature.entity.ServerCalendarEntity +import com.gh.gamecenter.entity.* +import com.gh.gamecenter.feature.entity.* import com.gh.gamecenter.login.user.UserManager import com.gh.gamecenter.retrofit.RetrofitManager import io.reactivex.Single @@ -23,18 +21,21 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import java.text.SimpleDateFormat import java.util.* -import kotlin.collections.ArrayList class ServersCalendarViewModel( application: Application, val game: GameEntity, val gameServer: GameDetailServer, - var meEntity: MeEntity? + var meEntity: MeEntity?, ) : AndroidViewModel(application) { + val error = NonStickyMutableLiveData() + val calendarLiveData: MutableLiveData> = MutableLiveData() val serversDetailLiveData: MutableLiveData = MutableLiveData() - val serverCalendarLiveData: MutableLiveData> = MutableLiveData() + val serverCalendarLiveData: MutableLiveData?> = MutableLiveData() + val serverSubscriptionLiveData = MutableLiveData() + val subscribeServerSuccessEvent = MutableLiveData() var monthCount = 0 var curWeek = 0 @@ -44,6 +45,7 @@ class ServersCalendarViewModel( var isExistCurServer = true// 当月是否存在开服 + var kaifuCalendar: Calendar? = null init { // checkExistCurSerer() @@ -59,21 +61,149 @@ class ServersCalendarViewModel( } } + /** + * 设置选中的开服表日期 + * @param kaifuTime 选中日期的时间戳 + */ + fun setKaifuTime(kaifuTime: Long) { + if (kaifuTime == 0L) return + + val currentCalendar = Calendar.getInstance().apply { + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + + val kaifuCalendar = Calendar.getInstance().also { + this.kaifuCalendar = it + it.timeInMillis = kaifuTime * 1000 + } + val kaifuYear = kaifuCalendar.get(Calendar.YEAR) + val kaifuMonth = kaifuCalendar.get(Calendar.MONTH) + + val currentYear = currentCalendar.get(Calendar.YEAR) + val currentMonth = currentCalendar.get(Calendar.MONTH) + + this.selectedMonth = if (kaifuYear == currentYear && kaifuMonth == currentMonth - 1) { + MonthType.PREVIOUS_MONTH + } else if (kaifuYear == currentYear && kaifuMonth == currentMonth + 1) { + MonthType.NEXT_MONTH + } else { + MonthType.CUR_MONTH + } + } + @SuppressLint("CheckResult") fun loadServerData(isMirror: Boolean = false) { - RetrofitManager.getInstance().api - .getGameServerCalendar(game.id, if (isMirror) "mirror" else "") + var apiService = RetrofitManager.getInstance().api + .getGameServerCalendar(game.id, if (isMirror) "mirror" else "", true) + + if (!CheckLoginUtils.isLogin()) { + serverSubscriptionLiveData.postValue(null) + } else { + apiService = RetrofitManager.getInstance().newApi + .getServerSubscription(game.id) + .onErrorReturnItem(ServerSubscriptionEntity()) + .zipWith(apiService) { subscription, list -> + serverSubscriptionLiveData.postValue(subscription) + list + } + } + apiService .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(object : BiResponse>() { - override fun onSuccess(data: MutableList) { - serverCalendarLiveData.postValue(data) - } - - override fun onFailure(exception: Exception) { + .subscribe( + { + serverCalendarLiveData.postValue(it) + }, + { serverCalendarLiveData.postValue(null) } - }) + ) + } + + @SuppressLint("CheckResult") + fun getServerSubscription(notifySubscribeSuccess: Boolean = false) { + if (!CheckLoginUtils.isLogin()) { + serverSubscriptionLiveData.postValue(null) + return + } + + // 已登录状态下检查开服表订阅状态 + RetrofitManager.getInstance().newApi + .getServerSubscription(game.id) + .onErrorReturnItem(ServerSubscriptionEntity()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + serverSubscriptionLiveData.postValue(it) + if (notifySubscribeSuccess && it.byApp) {// 发送订阅成功通知 + subscribeServerSuccessEvent.postValue(null) + } + }, + { + serverSubscriptionLiveData.postValue(null) + } + ) + } + + @SuppressLint("CheckResult") + fun subscribeServer() { + RetrofitManager.getInstance().newApi + .subscribeServer(game.id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + getServerSubscription(true) + }, + { + error.postValue(it) + } + ) + } + + @SuppressLint("CheckResult") + fun unsubscribeServer() { + RetrofitManager.getInstance().newApi + .unsubscribeServer(game.id) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { + getServerSubscription() + }, { + error.postValue(it) + } + ) + } + + fun dispatchNotifySettingChange(id: String, notifySetting: ServerCalendarNotifySetting?) { + val serverCalendarList = serverCalendarLiveData.value ?: return + + val index = serverCalendarList.indexOfFirst { + it.id == id + } + + if (index != -1) { + val oldServerCalendar = serverCalendarList[index] + val newServerCalendar = ServerCalendarEntity().apply { + this.id = oldServerCalendar.id + this.setTime(oldServerCalendar.getTime()) + this.setNote(oldServerCalendar.getNote()) + this.remark = oldServerCalendar.remark + this.setFirstName(oldServerCalendar.getFirstName()) + this.setServerName(oldServerCalendar.getServerName()) + this.type = oldServerCalendar.type + this.setDataMark(oldServerCalendar.getDataMark()) + this.notifySetting = notifySetting + } + serverCalendarList[index] = newServerCalendar + serverCalendarLiveData.value = serverCalendarList + } + } @SuppressLint("CheckResult") @@ -81,7 +211,10 @@ class ServersCalendarViewModel( val requestMap = hashMapOf>() requestMap["game"] = arrayListOf(game.id) - RetrofitManager.getInstance().api.getUserRelatedInfoByPost(UserManager.getInstance().userId, requestMap.toRequestBody()) + RetrofitManager.getInstance().api.getUserRelatedInfoByPost( + UserManager.getInstance().userId, + requestMap.toRequestBody() + ) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(object : BiResponse() { @@ -189,6 +322,19 @@ class ServersCalendarViewModel( emitter.onSuccess(curDayData) }.subscribe(object : BiResponse>() { override fun onSuccess(data: MutableList) { + val kaifuCalendar = this@ServersCalendarViewModel.kaifuCalendar + if (kaifuCalendar != null) {// 如果指定了选中的开服表日期,则自动唤起开服详情弹窗 + val index = data.indexOfFirst { + it.year == kaifuCalendar.get(Calendar.YEAR) + && it.month == kaifuCalendar.get(Calendar.MONTH) + 1 + && it.day == kaifuCalendar.get(Calendar.DATE) + } + if (index != -1) { + serversDetailLiveData.postValue(data[index]) + } + this@ServersCalendarViewModel.kaifuCalendar = null + } + calendarLiveData.postValue(data) } diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarWeChatSubscriptionSuccessDialog.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarWeChatSubscriptionSuccessDialog.kt new file mode 100644 index 0000000000..2c043e19b5 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersCalendarWeChatSubscriptionSuccessDialog.kt @@ -0,0 +1,58 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatDialogFragment +import androidx.fragment.app.activityViewModels +import androidx.viewpager.widget.ViewPager.LayoutParams +import com.gh.gamecenter.R +import com.gh.gamecenter.WebActivity +import com.gh.gamecenter.common.utils.dip2px +import com.gh.gamecenter.databinding.DialogWechatReserveSuccessBinding +import com.google.android.material.bottomsheet.BottomSheetDialogFragment + +/** + * 游戏订阅成功-未绑定微信弹窗 + */ +class ServersCalendarWeChatSubscriptionSuccessDialog : AppCompatDialogFragment() { + + private lateinit var viewBinding: DialogWechatReserveSuccessBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(BottomSheetDialogFragment.STYLE_NORMAL, R.style.DialogWindowTransparent) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val rootView = DialogWechatReserveSuccessBinding.inflate(inflater, container, false) + .also { + viewBinding = it + }.root + return rootView.apply { + layoutParams = ViewGroup.LayoutParams(300F.dip2px(), LayoutParams.WRAP_CONTENT) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + viewBinding.closeBtn.setOnClickListener { + dismissAllowingStateLoss() + } + + viewBinding.closeBtn.setOnClickListener { + dismissAllowingStateLoss() + } + + viewBinding.titleIv.setImageResource(R.drawable.bg_servers_calendar_reserve_success) + + viewBinding.openBtn.setOnClickListener { + it.context.startActivity(WebActivity.getBindWechatIntent(context)) + dismissAllowingStateLoss() + } + + viewBinding.contentTv.text = getString(R.string.servers_calendar_subscription_dialog_wechat_content) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailReportViewHolder.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailReportViewHolder.kt deleted file mode 100644 index ae7ac30072..0000000000 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailReportViewHolder.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.gh.gamecenter.gamedetail.fuli.kaifu - -import android.app.Activity -import android.graphics.Paint -import com.gh.gamecenter.common.base.BaseRecyclerViewHolder -import com.gh.gamecenter.common.utils.toColor -import com.gh.gamecenter.R -import com.gh.gamecenter.databinding.DialogServersCalendearDetailItemBinding -import com.gh.gamecenter.databinding.DialogServersCalendearDetailReportItemBinding -import com.gh.gamecenter.feature.entity.GameEntity -import com.gh.gamecenter.feature.entity.MeEntity -import com.gh.gamecenter.feature.entity.ServerCalendarEntity -import com.gh.gamecenter.servers.patch.PatchKaifuActivity -import com.lightgame.utils.Utils - -class ServersDetailReportViewHolder(val binding: DialogServersCalendearDetailReportItemBinding) : - BaseRecyclerViewHolder(binding.root) { - - fun bindItem(data: ServerCalendarEntity) { - binding.name.text = data.getNote() - binding.time.text = data.getFormatTime("HH:mm") - binding.remark.text = data.remark - - binding.name.setTextColor(R.color.text_title.toColor(binding.root.context)) - binding.time.setTextColor(R.color.text_title.toColor(binding.root.context)) - binding.remark.setTextColor(R.color.text_title.toColor(binding.root.context)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailViewHolder.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailViewHolder.kt deleted file mode 100644 index 15d442474d..0000000000 --- a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersDetailViewHolder.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.gh.gamecenter.gamedetail.fuli.kaifu - -import android.app.Activity -import android.graphics.Paint -import com.gh.gamecenter.common.base.BaseRecyclerViewHolder -import com.gh.gamecenter.common.utils.toColor -import com.gh.gamecenter.R -import com.gh.gamecenter.databinding.DialogServersCalendearDetailItemBinding -import com.gh.gamecenter.feature.entity.GameEntity -import com.gh.gamecenter.feature.entity.MeEntity -import com.gh.gamecenter.feature.entity.ServerCalendarEntity -import com.gh.gamecenter.servers.patch.PatchKaifuActivity -import com.lightgame.utils.Utils - -class ServersDetailViewHolder(val binding: DialogServersCalendearDetailItemBinding) : - BaseRecyclerViewHolder(binding.root) { - - fun bindItem(data: ServerCalendarEntity, meEntity: MeEntity?, gameEntity: GameEntity) { - binding.name.text = data.getNote() - binding.time.text = data.getFormatTime("HH:mm") - binding.remark.text = data.remark - - binding.name.setTextColor(R.color.text_title.toColor(binding.root.context)) - binding.time.setTextColor(R.color.text_title.toColor(binding.root.context)) - binding.remark.setTextColor(R.color.text_title.toColor(binding.root.context)) - - if (meEntity?.isPartTime == true) { - binding.name.paint.flags = Paint.UNDERLINE_TEXT_FLAG - binding.name.paint.isAntiAlias = true - binding.name.setOnClickListener { - val context = binding.root.context - if ("删档内测" == data.type || "不删档内测" == data.type || "公测" == data.type) { - Utils.toast(context, "开测信息不可编辑"); - } else { - (context as Activity).startActivityForResult( - PatchKaifuActivity.getIntent( - context, - data, - gameEntity.id - ), - ServersCalendarActivity.GAME_DETAIL_PATCH_KAIFU_REQUEST - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListActivity.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListActivity.kt new file mode 100644 index 0000000000..373cfa8262 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListActivity.kt @@ -0,0 +1,36 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.PersistableBundle +import com.gh.gamecenter.R +import com.gh.gamecenter.common.base.activity.ToolBarActivity +import com.gh.gamecenter.common.utils.updateStatusBarColor + +class ServersSubscribedGameListActivity : ToolBarActivity() { + + override fun provideNormalIntent(): Intent { + return getTargetIntent( + this, + ServersSubscribedGameListActivity::class.java, + ServersSubscribedGameListFragment::class.java + ) + } + + override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { + super.onCreate(savedInstanceState, persistentState) + updateStatusBarColor(R.color.background_white, R.color.background_white) + } + + override fun onDarkModeChanged() { + super.onDarkModeChanged() + updateStatusBarColor(R.color.background_white, R.color.background_white) + } + + companion object { + fun getIntent(context: Context): Intent { + return Intent(context, ServersSubscribedGameListActivity::class.java) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListAdapter.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListAdapter.kt new file mode 100644 index 0000000000..dc51c1c516 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListAdapter.kt @@ -0,0 +1,64 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.gh.common.util.NewFlatLogUtils +import com.gh.gamecenter.GameDetailActivity +import com.gh.gamecenter.common.baselist.ListAdapter +import com.gh.gamecenter.common.exposure.ExposureSource +import com.gh.gamecenter.common.utils.layoutInflater +import com.gh.gamecenter.core.utils.MtaHelper +import com.gh.gamecenter.databinding.ItemServersSubscribedGameBinding +import com.gh.gamecenter.feature.entity.GameEntity +import com.gh.gamecenter.feature.exposure.ExposureEvent +import com.gh.gamecenter.feature.game.GameItemViewHolder + +class ServersSubscribedGameListAdapter(context: Context) : ListAdapter(context) { + + private var onUnsubscribeClickListener: OnUnsubscribeClickListener? = null + + fun setOnUnsubscribeClickListener(onUnsubscribeClickListener: OnUnsubscribeClickListener) { + this.onUnsubscribeClickListener = onUnsubscribeClickListener + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ServersCalendarSubscriptionViewHolder( + ItemServersSubscribedGameBinding.inflate( + parent.layoutInflater, + parent, + false + ) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + if (holder is ServersCalendarSubscriptionViewHolder) { + val game = mEntityList[position] + holder.binding.icon.displayGameIcon(game) + holder.binding.gameName.text = game.name + GameItemViewHolder.initGameSubtitleAndAdLabel(game, holder.binding.subtitle) + + val exposureEvent = ExposureEvent.createEvent( + game, + listOf(ExposureSource("开服管理", "游戏订阅")) + ) + + holder.binding.subscribe.setOnClickListener { + onUnsubscribeClickListener?.onUnsubscribeClick(game, position) + } + + holder.binding.root.setOnClickListener { + GameDetailActivity.startGameDetailActivity(mContext, game.id, "开服管理-游戏订阅", exposureEvent) + } + } + } + + override fun getItemCount(): Int = mEntityList.size + + class ServersCalendarSubscriptionViewHolder(val binding: ItemServersSubscribedGameBinding) : ViewHolder(binding.root) + + interface OnUnsubscribeClickListener { + fun onUnsubscribeClick(gameEntity: GameEntity, position: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListFragment.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListFragment.kt new file mode 100644 index 0000000000..560c76c9ca --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListFragment.kt @@ -0,0 +1,80 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.gh.common.util.NewLogUtils +import com.gh.gamecenter.R +import com.gh.gamecenter.common.baselist.ListAdapter +import com.gh.gamecenter.common.baselist.ListFragment +import com.gh.gamecenter.common.utils.DialogHelper +import com.gh.gamecenter.common.utils.NewFlatLogUtils +import com.gh.gamecenter.common.utils.SensorsBridge +import com.gh.gamecenter.common.utils.viewModelProvider +import com.gh.gamecenter.core.utils.ToastUtils +import com.gh.gamecenter.feature.entity.GameEntity + +/** + * 开服订阅-开服表-游戏订阅 + */ +class ServersSubscribedGameListFragment : ListFragment(), + ServersSubscribedGameListAdapter.OnUnsubscribeClickListener { + + private var adapter: ServersSubscribedGameListAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setNavigationTitle(getString(R.string.servers_calendar_subscribed_game_title)) + mCachedView.background = ContextCompat.getDrawable(requireContext(), R.color.background_white) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mListViewModel.error.observe(viewLifecycleOwner) { + ToastUtils.showToast(getString(R.string.network_error_hint)) + } + } + + override fun provideListViewModel(): ServersSubscribedGameListViewModel { + return viewModelProvider() + } + + override fun provideListAdapter(): ListAdapter { + return adapter ?: ServersSubscribedGameListAdapter( + requireContext() + ).apply { + setOnUnsubscribeClickListener(this@ServersSubscribedGameListFragment) + adapter = this + } + } + + override fun onUnsubscribeClick(gameEntity: GameEntity, position: Int) { + NewLogUtils.logLaunchServerSubscribeCancel( + gameId = gameEntity.id, + gameName = gameEntity.name ?: "" + ) + SensorsBridge.trackLaunchServerSubscribeCancelClick( + gameId = gameEntity.id, + gameName = gameEntity.name ?: "" + ) + + DialogHelper.showDialog( + context = requireContext(), + title = getString(R.string.servers_subscribed_game_unsubscribe_dialog_title), + content = getString(R.string.servers_subscribed_game_unsubscribe_dialog_content), + confirmText = getString(R.string.servers_subscribed_game_unsubscribe_dialog_confirm), + cancelText = getString(R.string.servers_subscribed_game_unsubscribe_dialog_cancel), + confirmClickCallback = { + mListViewModel.unsubscribeServer(gameEntity.id) + } + ) + } + + override fun getItemDecoration(): RecyclerView.ItemDecoration? = null + + override fun onDarkModeChanged() { + super.onDarkModeChanged() + mCachedView.background = ContextCompat.getDrawable(requireContext(), R.color.background_white) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListViewModel.kt b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListViewModel.kt new file mode 100644 index 0000000000..caf1b6f5d1 --- /dev/null +++ b/app/src/main/java/com/gh/gamecenter/gamedetail/fuli/kaifu/ServersSubscribedGameListViewModel.kt @@ -0,0 +1,62 @@ +package com.gh.gamecenter.gamedetail.fuli.kaifu + +import android.annotation.SuppressLint +import android.app.Application +import com.gh.gamecenter.common.baselist.ListViewModel +import com.gh.gamecenter.common.baselist.LoadStatus +import com.gh.gamecenter.common.livedata.NonStickyMutableLiveData +import com.gh.gamecenter.feature.entity.GameEntity +import com.gh.gamecenter.retrofit.RetrofitManager +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers + +class ServersSubscribedGameListViewModel(application: Application) : ListViewModel(application) { + + val error = NonStickyMutableLiveData() + + override fun provideDataObservable(page: Int): Observable> { + return RetrofitManager.getInstance().newApi.serversSubscribedGameList + } + + override fun mergeResultLiveData() { + mResultLiveData.addSource(mListLiveData) { + mResultLiveData.postValue(it) + } + } + + override fun loadStatusControl(size: Int) { + when (size) { + 0 -> { + mRetryParams = null + mLoadStatusLiveData.setValue(LoadStatus.INIT_EMPTY) + } + REQUEST_FAILURE_SIZE -> { + mRetryParams = mCurLoadParams + mLoadStatusLiveData.setValue(LoadStatus.INIT_FAILED) + } + else -> { + mRetryParams = null + mLoadStatusLiveData.setValue(LoadStatus.INIT_OVER) + } + } + } + + @SuppressLint("CheckResult") + fun unsubscribeServer(gameId: String) { + RetrofitManager.getInstance().newApi + .unsubscribeServer(gameId) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ + val dataList = mListLiveData.value ?: return@subscribe + val index = dataList.indexOfFirst { it.id == gameId } + if (index != -1) { + dataList.removeAt(index) + mListLiveData.value = dataList + } + }, { + error.postValue(it) + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/message/MessageUnreadRepository.kt b/app/src/main/java/com/gh/gamecenter/message/MessageUnreadRepository.kt index 372d808464..a342e83dbd 100644 --- a/app/src/main/java/com/gh/gamecenter/message/MessageUnreadRepository.kt +++ b/app/src/main/java/com/gh/gamecenter/message/MessageUnreadRepository.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData import com.gh.gamecenter.common.constant.Constants import com.gh.common.util.CheckLoginUtils +import com.gh.common.util.NewLogUtils import com.gh.gamecenter.core.utils.GsonUtils import com.gh.gamecenter.core.utils.SPUtils import com.gh.gamecenter.common.utils.createRequestBody @@ -13,9 +14,11 @@ import com.gh.gamecenter.db.info.GameTrendsInfo import com.gh.gamecenter.login.user.UserManager import com.gh.gamecenter.common.retrofit.BiResponse import com.gh.gamecenter.common.retrofit.Response +import com.gh.gamecenter.common.utils.SensorsBridge import com.gh.gamecenter.entity.* import com.gh.gamecenter.feature.entity.MessageUnreadEntity import com.gh.gamecenter.feature.entity.ConcernEntity +import com.gh.gamecenter.feature.entity.MessageDigestEntity import com.gh.gamecenter.feature.entity.MessageUnreadCount import com.gh.gamecenter.retrofit.RetrofitManager import com.gh.gamecenter.retrofit.service.ApiService @@ -215,10 +218,33 @@ object MessageUnreadRepository { } } + private fun getMessageUnreadDigestList(): Observable> { + if (!CheckLoginUtils.isLogin()) { + return Observable.just(emptyList()) + } + return mNewApi.getMessageUnreadDigestList(1, 100) + .onErrorReturnItem(emptyList()) + } + // 获取新消息中心入口未读消息数量 @SuppressLint("CheckResult") private fun loadNewMessageUnreadCount() { - Observable.zip(getNewMessageUnreadCount(), getDiscoveryData()) { t1, t2 -> + Observable.zip(getNewMessageUnreadCount(), getDiscoveryData(), getMessageUnreadDigestList()) { t1, t2, t3-> + t3.forEach { + SensorsBridge.trackMessageReceive( + gameId = it.gameId, + gameName = it.gameName, + sessionMessageType = it.sessionMessageTypeChinese, + messageType = it.messageTypeChinese + ) + NewLogUtils.logMessageInformPush( + gameId = it.gameId, + gameName = it.gameName, + sessionMessageType = it.sessionMessageTypeChinese, + messageType = it.messageTypeChinese + ) + } + if (t1 != 0) { messageCenterUnreadCountLiveData.postValue(t1) t1 diff --git a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalBannerAdapter.kt b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalBannerAdapter.kt index 6bb07d4f9f..e1db85f6d6 100644 --- a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalBannerAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalBannerAdapter.kt @@ -205,6 +205,10 @@ class HaloPersonalBannerAdapter(context: Context) : BaseRecyclerAdapter {// 开服管理 + DirectUtils.directToServersCalendarManagement(mContext, "我的光环-活动位") + } + else -> { DirectUtils.directToLinkPage(mContext, it, "我的光环banner", "") } diff --git a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalFragment.kt b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalFragment.kt index 0cd8bc480d..9bcef958b1 100644 --- a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalFragment.kt @@ -169,6 +169,7 @@ class HaloPersonalFragment : BaseLazyFragment() { mStubBinding.loginMessageHint.visibility == View.VISIBLE, "我的" ) + SensorsBridge.trackMessageCenterClick() // 优先进入有数字提醒的消息tab,其次是有红点提醒的游戏动态,最后是没有提醒的消息tab val defaultTabIndex = if ((mUnreadViewModel.messageUnreadCountLiveData.value?.message diff --git a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalFunctionAdapter.kt b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalFunctionAdapter.kt index 1e41f9295f..ad89ac4452 100644 --- a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalFunctionAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalFunctionAdapter.kt @@ -199,6 +199,10 @@ class HaloPersonalFunctionAdapter(context: Context) : BaseRecyclerAdapter {// 开服管理 + DirectUtils.directToServersCalendarManagement(mContext, "我的光环-更多功能") + } + else -> { DirectUtils.directToLinkPage(mContext, it, "我的光环", "更多功能") } diff --git a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalRecommendAdapter.kt b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalRecommendAdapter.kt index 0e1f5db633..3b9d1ec15a 100644 --- a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalRecommendAdapter.kt +++ b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalRecommendAdapter.kt @@ -233,6 +233,10 @@ class HaloPersonalRecommendAdapter(val context: Context) : BaseRecyclerAdapter {// 开服管理 + DirectUtils.directToServersCalendarManagement(mContext, "我的光环-推荐位") + } + else -> { DirectUtils.directToLinkPage(mContext, it, "我的光环", "推荐位") } diff --git a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalViewModel.kt b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalViewModel.kt index 1ec7323350..ec5d745452 100644 --- a/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalViewModel.kt +++ b/app/src/main/java/com/gh/gamecenter/personal/HaloPersonalViewModel.kt @@ -34,7 +34,6 @@ import org.greenrobot.eventbus.EventBus import retrofit2.HttpException import java.text.SimpleDateFormat import java.util.* -import kotlin.collections.ArrayList class HaloPersonalViewModel(application: Application) : AndroidViewModel(application) { @@ -71,6 +70,7 @@ class HaloPersonalViewModel(application: Application) : AndroidViewModel(applica recommendData.postValue(it.addons) } } + "activity" -> activityData.postValue(it.addons) "more_features" -> moreFeaturesData.postValue(it.addons) } @@ -159,9 +159,11 @@ class HaloPersonalViewModel(application: Application) : AndroidViewModel(applica "game" -> { GameDetailActivity.startGameDetailActivity(context, data.link, entrance) } + "news" -> { context.startActivity(NewsDetailActivity.getIntentById(context, data.link, entrance)) } + "column" -> { SubjectActivity.startSubjectActivity( context, @@ -172,6 +174,7 @@ class HaloPersonalViewModel(application: Application) : AndroidViewModel(applica entrance ) } + else -> { val linkEntity = LinkEntity() linkEntity.type = data.type diff --git a/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailRecyclerView.kt b/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailRecyclerView.kt index 0557ba8114..58adef595c 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailRecyclerView.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/article/detail/ArticleDetailRecyclerView.kt @@ -1,14 +1,13 @@ package com.gh.gamecenter.qa.article.detail import android.content.Context -import android.os.Build import android.util.AttributeSet -import android.util.Log import android.view.MotionEvent import android.view.View import androidx.core.view.NestedScrollingParent2 import androidx.core.view.ViewCompat import androidx.recyclerview.widget.RecyclerView +import com.lightgame.utils.Utils /** * 资讯详情-外层列表控件 @@ -35,6 +34,7 @@ class ArticleDetailRecyclerView @JvmOverloads constructor( } override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean { + Utils.log("ArticleDetailRv->onStartNestedScroll->child: $child, $target, axes: $axes, type: $type") val handleNestedScroll = (axes and View.SCROLL_AXIS_VERTICAL != 0) if (handleNestedScroll) { if (type == ViewCompat.TYPE_TOUCH) { @@ -46,10 +46,12 @@ class ArticleDetailRecyclerView @JvmOverloads constructor( } override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) { + Utils.log("ArticleDetailRv->onNestedScrollAccepted->child: $child, target: $target, axes: $axes, type: $type") onNestedScrollAccepted(child, target, axes) } override fun onStopNestedScroll(target: View, type: Int) { + Utils.log("ArticleDetailRv->onStopNestedScroll->target: $target, type: $type") onStopNestedScroll(target) if (type == ViewCompat.TYPE_NON_TOUCH) { targetNestedScrollingView = null @@ -57,12 +59,15 @@ class ArticleDetailRecyclerView @JvmOverloads constructor( } override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) { + Utils.log("ArticleDetailRv->onNestedPreScroll->$target, dx: $dx, dy: $dy, consumed: $consumed, type: $type") // 该回调发生在内层RecyclerView滚动之前 // 如果是向下滚动并且外层RecyclerView未滚动到底部,由外层RecyclerView进行滚动 if (dy > 0 && canScrollVertically(1)) { + Utils.log("ArticleDetailRv->onNestedPreScroll1->$target, dx: $dx, dy: $dy, consumed: $consumed, type: $type") scrollBy(0, dy) consumed[1] = dy } else { + Utils.log("ArticleDetailRv->onNestedPreScroll2->$target, dx: $dx, dy: $dy, consumed: $consumed, type: $type") onNestedPreScroll(target, dx, dy, consumed) } } @@ -75,12 +80,25 @@ class ArticleDetailRecyclerView @JvmOverloads constructor( dyUnconsumed: Int, type: Int ) { + Utils.log("ArticleDetailRv->onNestedScroll->$target, dxConsumed: $dxConsumed, dyConsumed: $dyConsumed, dxUnconsumed: $dxUnconsumed, dyUnconsumed: $dyUnconsumed, type: $type") // 该回调发生在内层RecyclerView滚动之后 // 如果是向上滚动并且内层RecyclerView滚动到顶部,由外层RecyclerView进行滚动 if (dyConsumed == 0 && dyUnconsumed < 0) { + Utils.log("ArticleDetailRv->onNestedScroll1->$target, dxConsumed: $dxConsumed, dyConsumed: $dyConsumed, dxUnconsumed: $dxUnconsumed, dyUnconsumed: $dyUnconsumed, type: $type") scrollBy(0, dyUnconsumed) } else { + Utils.log("ArticleDetailRv->onNestedScroll2->$target, dxConsumed: $dxConsumed, dyConsumed: $dyConsumed, dxUnconsumed: $dxUnconsumed, dyUnconsumed: $dyUnconsumed, type: $type") onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed) } } + + override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean { + Utils.log("ArticleDetailRv->onNestedPreFling->$target, velocityX: $velocityX, velocityY: $velocityY") + return super.onNestedPreFling(target, velocityX, velocityY) + } + + override fun onNestedFling(target: View, velocityX: Float, velocityY: Float, consumed: Boolean): Boolean { + Utils.log("ArticleDetailRv->onNestedFling->$target, velocityX: $velocityX, velocityY: $velocityY, consumed:$consumed") + return super.onNestedFling(target, velocityX, velocityY, consumed) + } } \ No newline at end of file diff --git a/app/src/main/java/com/gh/gamecenter/qa/video/detail/ForumVideoDetailFragment.kt b/app/src/main/java/com/gh/gamecenter/qa/video/detail/ForumVideoDetailFragment.kt index 3c78dbf8bd..725991c5ad 100644 --- a/app/src/main/java/com/gh/gamecenter/qa/video/detail/ForumVideoDetailFragment.kt +++ b/app/src/main/java/com/gh/gamecenter/qa/video/detail/ForumVideoDetailFragment.kt @@ -365,7 +365,7 @@ class ForumVideoDetailFragment : BaseFragment_TabLayout() { mViewModel.currentToolbarStatus = isToolbarWhite mBinding.toolbar.setNavigationIcon(R.drawable.ic_bar_back_light) - mMoreMenuItem?.setIcon(R.drawable.ic_menu_gamedetail_more_light) + mMoreMenuItem?.setIcon(R.drawable.ic_menu_more_light) mBinding.toolbar.setBackgroundColor( ContextCompat.getColor( requireContext(), diff --git a/app/src/main/java/com/gh/gamecenter/retrofit/RetrofitManager.java b/app/src/main/java/com/gh/gamecenter/retrofit/RetrofitManager.java index 573c0ec0d9..7d8f3fb14e 100644 --- a/app/src/main/java/com/gh/gamecenter/retrofit/RetrofitManager.java +++ b/app/src/main/java/com/gh/gamecenter/retrofit/RetrofitManager.java @@ -3,13 +3,10 @@ package com.gh.gamecenter.retrofit; import android.content.Context; import com.gh.common.constant.Config; -import com.gh.gamecenter.BuildConfig; import com.gh.gamecenter.common.retrofit.BaseRetrofitManager; -import com.gh.gamecenter.common.utils.EnvHelper; import com.gh.gamecenter.retrofit.service.ApiService; import com.gh.gamecenter.retrofit.service.VApiService; import com.halo.assistant.HaloApp; -import com.lightgame.utils.Utils; import okhttp3.OkHttpClient; @@ -30,7 +27,7 @@ public class RetrofitManager extends BaseRetrofitManager { mApiService = provideService(okHttpNormalConfig, Config.API_HOST, ApiService.class); mNewApiService = provideService(okHttpNormalConfig, Config.NEW_API_HOST, ApiService.class); mUploadApiService = provideService(getOkHttpConfig(context, UPLOAD_CALL_TIME_OUT, 1), Config.API_HOST, ApiService.class); - mVApiService = provideService(okHttpNormalConfig, Config.VAPI_HOST, VApiService.class); + mVApiService = provideService(okHttpNormalConfig, Config.VAPI_HOST, VApiService.class);; } public static RetrofitManager getInstance() { diff --git a/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java b/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java index 3bbd444faa..f456ac1875 100644 --- a/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java +++ b/app/src/main/java/com/gh/gamecenter/retrofit/service/ApiService.java @@ -19,6 +19,9 @@ import com.gh.gamecenter.entity.BlockEntity; import com.gh.gamecenter.entity.CarouselEntity; import com.gh.gamecenter.entity.CatalogEntity; import com.gh.gamecenter.entity.CategoryEntity; +import com.gh.gamecenter.entity.ServerSubscriptionEntity; +import com.gh.gamecenter.feature.entity.CommentEntity; +import com.gh.gamecenter.feature.entity.CommentnumEntity; import com.gh.gamecenter.entity.CommonCollectionContentEntity; import com.gh.gamecenter.entity.CommonCollectionEntity; import com.gh.gamecenter.entity.DefaultAvatar; @@ -52,6 +55,11 @@ import com.gh.gamecenter.entity.HomeItemTestV2Entity; import com.gh.gamecenter.entity.ImageInfoEntity; import com.gh.gamecenter.entity.InterestedGameEntity; import com.gh.gamecenter.entity.LibaoDetailEntity; +import com.gh.gamecenter.feature.entity.LibaoEntity; +import com.gh.gamecenter.feature.entity.LibaoStatusEntity; +import com.gh.gamecenter.feature.entity.MessageDigestEntity; +import com.gh.gamecenter.feature.entity.MessageUnreadCount; +import com.gh.gamecenter.feature.entity.MessageUnreadEntity; import com.gh.gamecenter.entity.MyVideoEntity; import com.gh.gamecenter.entity.NewApiSettingsEntity; import com.gh.gamecenter.entity.NewSettingsEntity; @@ -87,6 +95,11 @@ import com.gh.gamecenter.entity.VideoDataOverViewEntity; import com.gh.gamecenter.entity.VideoDraftEntity; import com.gh.gamecenter.entity.VideoEntity; import com.gh.gamecenter.entity.VideoTagEntity; +import com.gh.gamecenter.feature.entity.ServerCalendarFormEntity; +import com.gh.gamecenter.feature.entity.ServerCalendarGame; +import com.gh.gamecenter.feature.entity.ServerCalendarNotifySetting; +import com.gh.gamecenter.feature.entity.WXSubscribeMsgConfig; +import com.gh.gamecenter.feature.entity.ViewsEntity; import com.gh.gamecenter.entity.VoteEntity; import com.gh.gamecenter.feature.entity.AnswerEntity; import com.gh.gamecenter.feature.entity.ApkEntity; @@ -116,6 +129,7 @@ import com.gh.gamecenter.gamedetail.entity.BigEvent; import com.gh.gamecenter.gamedetail.entity.NewGameDetailEntity; import com.gh.gamecenter.login.entity.UserInfoEntity; import com.gh.gamecenter.personalhome.rating.MyRating; +import com.gh.gamecenter.qa.entity.TopCommunityCategory; import com.gh.gamecenter.qa.entity.AnswerDetailEntity; import com.gh.gamecenter.qa.entity.AnswerDraftEntity; import com.gh.gamecenter.qa.entity.ArticleDetailEntity; @@ -1979,7 +1993,9 @@ public interface ApiService { * 获取游戏开服详情数据(注:和游戏详情获取到的开服数据是有区别的) */ @GET("games/{game_id}/server_calendar") - Single> getGameServerCalendar(@Path("game_id") String gameId, @Query("view") String view); + Single> getGameServerCalendar(@Path("game_id") String gameId, + @Query("view") String view, + @Query("refresh") boolean refresh); /** * 获取游戏上传提示 @@ -3225,4 +3241,84 @@ public interface ApiService { */ @POST("/users/games/{gameId}/archives/{archiveId}/replace") Single replaceArchive(@Path("gameId") String gameId, @Path("archiveId") String archiveId, @Body RequestBody body); + + /** + * 游戏开服-加入订阅 + */ + @POST("games/{game_id}/servers/subscriptions") + Observable subscribeServer(@Path("game_id") String gameId); + + /** + * 游戏开服-获取订阅状态 + */ + @GET("games/{game_id}/servers/subscriptions") + Single getServerSubscription(@Path("game_id") String gameId); + + /** + * 游戏开服-取消订阅 + */ + @DELETE("games/{game_id}/servers/subscriptions") + Observable unsubscribeServer(@Path("game_id") String gameId); + + /** + * 游戏开服-开服提醒-添加 + */ + @POST("games/{game_id}/server_calendar/{item_id_or_time}/notify_setting") + Single addServerCalendarRemind(@Path("game_id") String gameId, + @Path("item_id_or_time") String itemIdOrTime, + @Body RequestBody body); + + /** + * 游戏开服-开服提醒-取消 + */ + @DELETE("games/{game_id}/server_calendar/{item_id_or_time}/notify_setting") + Single removeServerCalendarRemind(@Path("game_id") String gameId, + @Path("item_id_or_time") String itemIdOrTime); + + /** + * 游戏开服-开服提醒-获取 + */ + @GET("games/{game_id}/server_calendar/{item_id_or_time}/notify_setting") + Single getServerCalendarRemind(@Path("game_id") String gameId, + @Path("item_id_or_time") String itemIdOrTime); + + /** + * 游戏开服-获取订阅游戏列表 + */ + @GET("games/servers/subscribe_games") + Observable> getServersSubscribedGameList(); + + + /** + * 游戏开服-开服订阅-开服表 + */ + @GET("games/servers/notify_servers") + Single> getServerCalendarList(@Query("page") int page, + @Query("page_size") int pageSize); + + /** + * 游戏开服-开服订阅-开服提醒 + */ + @GET("games/servers/notify_games") + Single> getServerCalendarRemindList(@Query("page") int page, + @Query("page_size") int pageSize); + + /** + * 游戏开服-开服订阅-获取微信一次订阅消息的参数数据 + */ + @GET("messages/wechat/one_time/config") + Single getWxSubscribeMsgConfig(@Query("filter") String filter); + + /** + * 游戏开服-开服订阅-提交微信一次订阅消息的授权结果回调 + */ + @POST("messages/wechat/one_time/call_back") + Single postWxSubscribeMsgCallback(@Query("filter") String filter, + @Body RequestBody body); + /** + * 未读消息-埋点数据提供 + */ + @GET("messages/unread/event") + Observable> getMessageUnreadDigestList(@Query("page") int page, + @Query("page_size") int pageSize); } \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_menu_gamedetail_more.xml b/app/src/main/res/drawable-night/ic_menu_more.xml similarity index 100% rename from app/src/main/res/drawable-night/ic_menu_gamedetail_more.xml rename to app/src/main/res/drawable-night/ic_menu_more.xml diff --git a/app/src/main/res/drawable-xxxhdpi/bg_servers_calendar_reserve_success.webp b/app/src/main/res/drawable-xxxhdpi/bg_servers_calendar_reserve_success.webp new file mode 100644 index 0000000000000000000000000000000000000000..7ed2745f855785b1311e543ee49a5ffc080fcebe GIT binary patch literal 73386 zcmcGTQpxykBj0%z6w8uLcEFwYFtsWi+)3K%$fERl|thE2! zLc3Grn~BZa?^UOmX9rC6t97f;oiT5nC4&T2$(e4qJoja&BJRj*TPXbA=lslc&g8S&pHtGdDg!Qj7tx~NLd-+Sw= znICoKGgt>Rl%nLyEQRJyHo)(LexUp>E)zxU?k#e9efM9#FeXa6Sxl@A=Mr1lKhmvt z#H{K#TRU}4brvQnBhQ0bC~E(U$0%V)*g}r{`)5dqLX@bCIXS{d^o)F~p2?=3`4Ke$ znIajal%C#xdr0xZ0cc0`hK`dkr?Sm6Cx8CK%%eAi^CM=Bts~(hB5nSlChN(y*TeWq zkvaNj_(;Rp-5tl!Pr3b%Bci~_c0L-;=1q&0ScjURVkOP?pP*OSDu8a){U)bLWuYzZ ze?;xnr{_7ZZQ_PNiF0st7r;9`>+j#|aL4s7WfH$S=OBr>Klk`{9vN7&S^mGq^SE>r zP`Q1&h;=vmCo={c6h~$F5d5VXJMcU$29d@3#FSIIa3a_Xr(?!%T$pyg(6CcKO^s^P zC1h6LY=d>LlF478+lJ6C-0;ZKvNSRMFi!<=I`Sk_z&o-$s5bJY147d z+=wW$`}R2KteMSL0#2j@ww^I#Y`RtszDFT^19VCB2%9Z?u$IjHaxqFoF;fjNA~G|6 zVEC&VR5S#}yGV17`W4SA&V0ux^3eK$Ru}f?+0`TNy2n9D=rU-WpEAblBNnp+F@VFl z?-6}@dAyx;2_-S$KtaD##y$4BVU4i5o9;ryRmkP766;rM0EJX?BSUHIAp5P(LF8LM zM+>L;P_Bn2M11LV`(a@n|G=dHr%_^iFRwD1rXiQA{%f?H!@@=7Gbc5%qLwqYtzQ=H z#A3`}WGjh2>LD0x-TEaLCZ|BRU52LQ2`1zXvMrA3nq+i<-l_e9F3icCm0`lqX!@gn zVf5ZpiD;@){c?unEaa1;l8x!wHY_juCRv-zhgXhjWD`-DiN%x&;QkX8ZmSuRA>>bibgPU&*G(!ZodZ`c~(r ztfCXbb@jT@kRl}O{A1}ocZFECbF0&vlZOIHu4rjHTNWcpi3-eCa|mKW5GxZu+)pdA zF355GVEv~qk6eEl2(_wu^R_94%4Q`74JVj8`WG1r@5dH~p?CsgVWL%?X!78+&!4)4 zCq51IAFzU!o+8znV(iW0&BT8x{<3x^ci%?7`m|0_TjPi}i~jwFFp=LZ+eR8yfgA18 zVmJV!2|#V1C|R@&Eb?{3tL~HgF;5#AZTxEq^x(#Jlb{LwwAT z=;n;BOJ@#U*BD>QXUnazW`?MCR4tHdnZ)SRP;hq36@%1|8v)nq5C$fFv)x?ADW+(u zac=ZtqaoK1NeRk3ysAGl6gB9@3%D$elx$5ceR`RR2KKuQ)96ONtHv@|t*Vb^o}WM? z1-N|U4}E%}NaTd%D9F{!emb%*`EQ3m9n0{Pz<0aNjA$*BbV8-Kjg1?2G6IhKz1hnR zp1-S|#zXfsv3eCD;jhuY2^f@WE1dE9sfwfJe9+; zEa}mIEi)lAbYe~~Js()?E7X=ig(voditDAa@T9PbG!ek>#6gO8W(LcoXm;a-Xy$O> zxs=PKbK}U@SMUwubJET@T`=}#evfZXd_7Vz5WOGlew&iN!Ley_`$;~Ky-EIK$xIK$ z=X6e2>k)X$2DV4d1xlh-URxZhN3T0R>e@PWz+PThAme7DJ7n>@8=O52kEJFBV)t!D za{O}T^PJs-2Ks=ncA3wb+P@}C(L|OOLybF`0s0KR>-#A@wk3XpvZIccxTDDYa;YFs zwt(Zg_*raLbpP9MMI;HLg@Ib+Is24}clQb-^ zKFH#J!>LEBFyX$s$Hc2y);7~1?dF+s_QyB#z4!aEROV%jc_-38HbB4b&DYy{ zY9mGBwRUdg{Qgx>@Ruat%f!7YB2p$b5IPRI{TQH_>I?yJO4+`H;j~OC00{yh4%~r z^P$-lEv@Mv3=oeum^r)x$ITo#RE9e&jxO1rMyp zk+gdwbeSgNfK>+PqE4WGVAlcHT`h1)nOctaXLOaSM!67otpxWs29E>(>7PD-?Kz~| zHZH;jciTL3GKt#1s?qban1eF%!hwW0U)lIKnqdTeJs27Sw+KUlS5qO3Ouq7U2@ck0 z2tn*xRvj(4lY7riL0GaJBWVPed9l^QujB=Zi z6v;M>A%OQ2brruBpR7M@^An2zkY7|87?nmV$~BhM3`Kak`Iu8fy`?4;3v;FkMD~W` zH*XD?A0&&UxYuI_{Xs{3rSYQ0|3+A!RwUDjuf-S@yxDpF{Z4krdTp1qwG_{dyz;*^ZX}fp zKBtQ`7p^PsB=dG@70oTFPqZMT=3qH@wuLLan+nvVC#4uE;ax*C7t7a*-yhk-}otnShX(+qW@n91y zZXXSxizLELQYbSQ#fA}1oEpzdz>1VZ%{JKVOolnD3FiF+%Y~)9g^7K571oLHr9hvO3 z=%F{KknQTJ&SNG@?Fk2&lJAjy*vy2~Lm3;{e5_<#*}+);A`LOg54G364c6n}y6D1X zUc1jrSY;z!sMI7vyy{Ae__uwj@9ZU*-9l=HQV9Ptb?#ih8y;B%cu;vqk1}L#2n94%`V6|Be1JZ8S;-vCmxoe}& zgkXt_Ed)Dps%)@bh(66~>Muk5MXn`&+$CVl;GT>rQY|@tQdedyAr$#wOA?Q=@23|$$Wz9-0I9(YL$unF%w~H(;~5puxI2}jVzcO_;I_V+(m!;JJVze zqq1T=A$h-Jp=Ai!=32L$SS1}`3~FI!;felk4q=G;9^+N059-p{teglho6WjVAPpzPPl-tvZf2*-i2Vv$Hxem}*$;upT6~-?}y3tq$SW2Vlx$^)o^_ z=E@O|rXvzJ$>l5wHX?(PGb}86z(o}5PZsnN26Kg989u=h^-^=Bd8#qOW#)f9y?zhA6*;(s@Ix9(PH@D`=yl5m^{QXR{0JnNB@?z{A&rsIO`;PvEs;{SD;qF= zOcf>VKH>Q&k@2N0wKJO!@FAu7db1X0sA%?$f^|DUdPIz%CB@k#GjhK&wnRFo6^wf% zzLB+mso2pJpPxImj!rD!Y#eHlKvSs2*70HZr`XP29nuX)8;Fs*lo@Z8l3tHHBy;_> z2q0{yO3K|d?XN1{>U^zp)IiFSH@;z{s4uieDa1&fUDJva%~E;sALeI4R#uxmJl2Un zMFw$Y7qc(*kG`1Am<^jG@1E85K+ea7fBP(wACH|DIRWs~d1lYp3)@{WhH;%MH3%Vl zE#?poF7XCFY@*N4I1{BfSY!P>JP$}poi>i6z9?yKl2Cbxo#V$cklyH9Ew6FI6Q)D% zDBrgcBqC+Sb3S_$E=j8aJ0`b9312f!W`_PfifJEOfKIRn*8;qP|0AG_a&Npz%Quz= z;TBV#rti_?jZ@fThf7iNAO;G`g<NI4dufF^zIM6T^AM?45ePDYx1*l@W0`88k zcUTJPFPx2!JRG$S=wzxNf$B=Rq11=vwntXODr`NT)HI=c0sG$?D%Wdarm6NqcP)Mq z{BdXMZSPq*Qxv*zRTY!8pXy*DLQLSajf_>6CdoJn`7D+N;HGeSD4$3EuwsCrSlsJ9 zA3Fbk1Yjgu`8AHBn!~1e9Tx8=R2n z(O#Jm#&QMoAr2=_)RCWt`=&v$i%>TWTGFc8qo2LqdLiuS>weEBV+W2Mer^Lw#wBf% z@11W%K((Hb%uJ=U*dED#@L41he=C{ca-s3ZBQAzH6Bnj{k#r-cgWy*v@((CH=JwZj#JrQ^0?NmXMXUn#Pk% zL$z|6wJIKyw%Y8v+cCA!hO7(4FcA8fh+12~H!h$fFK%`<6|btkU=^ccG;VVHQmRSt z_G|`rRpQp5Aq7D!rDk^AIqkZ$PtedYy&2IK)y7Ew#w7(iOm?wWXI?kYny*iE3@~gs zp+Q|@IX6MX+OU3=JnYB)5q~Awhmsm=HGR(ZA(zLSzN!ILjk{6Nzi?tm`Yr%-ky!fG zl5^g7#Y17gY*yvyq&3vdynEXRuN~W{NsX3&XP2jtZzgyPR{br+l{6PrT z^NUV^(;^UtgGDSpLCOze0CADXN8aJapxeG@e?*gM-^2ZsDJA4|tn<&uC;Ed4f~VZ9 z>l2x1j*x_ZsIp0}hlUYRgt~1Zv+c*5#G~^@vhC!P!;Det{d_jsf=ao2c^h3@Hmbn{ zLL(bS1s|yMZIkzp^TF1y4th0f8dT^a)UURuq$Z+uv%j~+d0-sw=6N5lMQ<;td&Hx} z5-Sg<)D+6_7tlv1g12zBSt++vZmk7BFr@&agmrh?w1-zpR(D8hIokbj3CCFmC{Xg7 z%R?$82$*;?oJa+AwbSX=g7@5v4l_iUwnaKKpk2+3c7jpn!eI2(K>X?EfdK@c3Y1SmjAlkpD=G~%xgv>=t zO*ch)b^K~(K5haSp^sf|&Ksy7#!62jMS7RWgE2_$vOQM=>-X*jL@%mp-e78rBK>JS ztx9q9$y0Gd*(WK(24Mv8{|4}HXVF4#36BHRM=eHAH5~hOo#9VW<6OLd>Dbkm5hS~+ zV}oWzV0_Mi$x>Mnwas6e_qOm@Ur*E*r(&Tugvv@{;JX^6)wpswuTt;G4W(8A!@_Gt z`Th1_$2dPJmhL+S96BO(94O7+c6yWe&oRA^SzJYNMgztV<=wax$>LUNEJno4nomoT?cP$Q^){1Nsq%-ujoA{Df6$x>CL>RC2YRNmCx*;ass?=?Yl3&z zb9!9^AR4jISLVpA^agkdmfb->x5+@VdaE>qU-U8er^US}Pd$zXUXA`_`Vx!yC@;NS zdar&qx7SX?|-IwS^TCzBiQ#;>x6z=3qZWx|&yaXo*grKNkUirpT50-i^F+E{07DkhhV}D3> z-t(!oP^Ucecz*2%_^FSz2s%k_2QbLw-C??jdqz7m&KMEVk=&yzVd7q0AFEB)gFSK; z&07j9996}xMRMiILV-ZCF{;7DdcVHUmTQ)T3Rvcs)|E}f6<;`G}SNVKVDUr$JLyc{C+(j&{p5Ea9xbSKQ5u#T{Yx+3v;#7sL}f=S!cq26R!nS zP;d9|;!|@W@xk==DilCFI;U%BV^jUOj>h7U^OV?(Jutr35*0mA2>W!Qm+t!B5j(IR z$vT~Bk_ZxrP{}$_aKlnvf*C}}xF)jeu^hu9SJdjOt?6yD=_4hi4%OpgSOO3NbtzI( z1SZ&wGfa9rxK?&ic+3it^J6!jM|DK{ON5q>1MPo|SPR}DMk01lGovrYdw#*VQj&+& zOIU5~nh1leRj;X`wJ5ig@tIB7#hA!-ofj%S71Cr}$%wI=YHSpVjYqe(sKNXd4(|1`^~dr>K<{^!`*=~(CQd&+SlwrLVpuQV}YKw3)4cY#d z6b$pJuF{&%{A*t#t&q$b2k+=r3`qph9ieYkdz3Xb{q?`^QbG|eU9GbKx)Dkb36%a5 zhzcusK7sht9?{Jp^&H1K3_~O+&M*Ot*iE%+BKfnU;>Y@Z()L6Au47{j5^0ml5wB;z z&{guxmgC~QGu6xcDmIok<;SMj7at|}K^~#bmll-d=WbGZftjs-RiLu8n)(AFc0+CC zB9&c`9wUTUvc1!fq~~c({#w9QoW(^w!SOKRB+T8`Ry&-uO;ZYuE?Q)Jqw>HELQ`x*!nyNub-_X%v<+ zeD*|ybn2bX0xV#}-xTA}tUfIibt!ya(ZUawJ);Jg8U=vjH=6i2-emCf-qE%uVBVwJ zn)t8@+R;}S2divSofL_M23}|<&aSC>p67`h%gZNpy;t~Kc_x$8Aw_lP))z~|h+dPG z>5k6>wVcbWv;Rl+tK9%|AtuiwAtL=ms;)wFDUh8)C^c0N%RKt0VEUVc>C-(g59BT9 zi~YF1TF-cDm^=~5xa{HssAx)$ns<3HU_ks9{cuF03-)Dn2%KB!Ufxac<%-pQg(*ux zq{|1j5rKVmTSRDpYiHI<7;Xq|9%B_6bAQdoEYq`8B(IT+9mjO_O^ zobt($Z4#h~H;EcC#AkQpq1bMT4G)Hkm2+iR6+7 zjDE4R0>U8FG)WuwgLxEuN772TBhK#UI*>p8kp1T{+|42p$_9AMDWyT}5Ho#zsk^0l zX7i!&r4%!llSmWI^H*v@FaUx`uS63|>82BIvLHqI^O6Nw0L zB?h1$va0gM>`T02-pu!5XUhe(^m0qf`Fz~b1}7`JSyJbyWb^|ahe#3fI1=+uy~+DQ zRF2jz!~k*~Eu|YUMS?duX3;uS8S?LDYuBsrWy>5<32$>jG)(th$V>cMOPD* zT8UUl=khA5*PV5iuqd%9YyB#4`^g)~HuzVDVUzBAm|ciMo@_5OeOOA#puN#r%C8$u?NrtV)-^eq*Uf2U!bBsx%iS_iGqHlaAl%ZGCYO z5%t*F#~vQ1gj{qc@8IiE#^>{+I#yX6@jUK$8_2X0)hX??a9C4V|_vkzh>O^EdUE8h?rkapd=Xb&JK4WRe?E( zYZesQLutF$ID30whg*rhMI~!fiooFQ0DBx3#$QId*%+yb5Um4$J@r}D@1kEEFeloAlM)Tb{%Iv z+O>@9&1*lIK1tnuhiDkEDexFffm=Ek1l$72X0*JW>pPYf{_@0(TZB0)gz}HH=b!cA zCFU5oz+9U0iB^(vtxzDAL-CfoQ{|_zAfw#K<-kacnx}3<@%Y3YhVKRk!S9JVDrF?L z0F>+u!I17f1PV>G-M8e}`w0X2Ud8{3@%LJJRV~Uy?;5ZhwK9Chy@WaZX z(HOU|dOdRYnJ0_w1qANMpKkCiTu~sk_}vPDs9~hm0feC3C#7kMRO8j5xl;Tf?Yp+q zcdMB3UFU+ptLu?`j$1oF-!kzI_5Z$qs{kw&fv_flKc|higZ$$#k?>Y#vy)fLxj$i= zLlBZjP=9DAFJ83uV=Gh;D>e>W_J#t4W%LG^ z+j-Skas8ZD!ASVbae;U5lDkq`Tu^>=kNDc=mfS&$5Z@OL(H9xuSNp6_)Tb`FK{1cZU;!l+~SuX{4O66$Z+N(klkTjeX7s?k~r7n@s zc<2Ub)~x}JqPM%`!0&Oh19f#)$q8tnvbPXn( zQ)Ysul|?vxlI%CJei~3@4-N|r@=fBYOl@?Whs+gkBOH?)olUAH^#Laq-N%&@i{-p7 zjxh#JZI%9?eM0Jsb`70k~V(s;O7=2cw8b z=uiCK^V^A-E*n2obAlMO0P9pw)Gn+a*f)yHAAUT`^e+(Jv`gP1b&eEjpZ2Os7zseY z;~ekIGSFZcVl3L}T&TQ>GVl&nfc&yl!Rcjh3k3?WV=0ba;NnX#YK4H2A!#3QVN^;I z6INtOMy_tTW^>RZM-`Cn7gg>sAfy`%gKt=ZY%>`!^Vi67>D*2YAOW!~)ah~ngL=-e z?fwC=?e7GU-=Vh2`yxYSpvb_v*Y@^3$uzESsG*<9-pllo=yvQ=4v~B|5%c~$HOYW+ zVCY9ha_}5Do&{6Z4Os+Fdl3H#jN~+^)4ze*Ay)c8$p1>n;=^&@~Cpfxdy+m}0Pe2<0M4gB_-%Lj{$F&>9n>(B1a8lPM;j9^qkr zKm>G+4dGfwc7Rd^js2s>Rmi5@ne{)c=}I8DE#mvc}5ARG&!bhofmxEBTN?piN=P{}`Y z6?yW@LcqcAr=hn%S@s!fQQ(8d6*|L=a(-z>YyxxM_(#1*yt?FR**spa3Qa@D)3NPp z(+&C*Dw_QSZYFF99OXCE_`neSf&EIQq{~QT=w3iw zsuTPhD$ExIY#>8yIJba9=R-JN+feazVIh8oRllEuIsxbOnInStVK2q^ma7F z-uP@JoaEyDWzZVgC;R7Eejz8Ttb>IJ)z0`=QuXd5@q=e5{x>bWp@(DA%4=&gDN zwxGVq5<6q#FDxYXoQ&0N4JzzRp`?;1#qLI;VNI`V-$+xs^9D#^QkbjajilD79cqLX zx%A|y5-uF`DJW$JxhG8!m3fFWros62GpMWxW-oH#)rj(!DlxDmTIBIgH1dk};&-1z z@N9e)j;@~^#y~#u=QLCrD9c=Jvq&#CO`mI3FHj#>g$vsH^F@V^XjuDnGCli5D5H9@ z3qLXg`lC5bcg)FmEKKx!LOjdQsM z4cbqB46f;6VBsuIRsn`DgP-^$@EDFIeO2iZKVLPWSY5!6i;Kba@S+IgcNH9EJ8wws zo*bOf{hlLuigzm|GO361?P!X8FQ%tkdW;LnA+zt>VCXTnNJuH*T?I89V zhDT{)qh54J`$$|^A+`&Ig~`$IgNTF#DMbJx-5W}9bptOjh!Fr$>}JTU0KjvVXmMfXSO~@7Hc`Y09p2D-U9*d?GA&2mSSvI!BKm6|k3zyE zc;fvHpkC6RF7huB^v`v>vc+c`3a&U_AFPn%5nQ@hTcYC@XU|8< zf=p}po%4U?nVYe%^>lcKw(NAn^8%53_xDRMUdz(o09@~zBkm01(Fp#cUloCsB;n1T z1T!#a0mwg;@jwcg=p7wXG-Yq~3M1iXBg=!8hLcnd=4+ytEC}dI_ZIM|Z3=@h_2G)M zdTa1kWPV9aFdqt_l*UH!QhYU*FC=LH&x?e4<1(rLO1N? z0wHX#WyZPYjG56wARI!RT}UPmOtaQ|jglk~*EIR&GpmXa{@JYIOc;mnJhjc0)Frxg zid;_>6uLA6d<;Xx1w3?vr{EMOe~PeKlB~FAx~cQ>9XR6xrAN>uF&rE!YZwx4#Ue)K zx6xC>!lfJl?}#@_C2Y6oT!CKemhUeKK&D6Kuf1^hQKZ%Ku<~RhUe@PTQ}4HbD=Yx1 zn6Ka|7DO!>Br%e2<0xypdYu~ z$;~tim$CN9B>X3^PQDfh-5{uaNI&YKph|gqvHxVIK(6qU^HUTaqMHnT=p&iQlpT4m zYU3S1*S=>BRp%@fly(TAg&iS5+mIK0Us@c^_|Bce#NT{%pb0Wkz%}SI;aj%7 z4^$SWnCH)GQ-==FOI|l{GJn>qB*Twvj{(Ow&Q>*PSDV!*mk%HQ#OmS~2J$~wfZk#( z*zg)QgN~}^ap*|pk4ET2wBNLxK$b1GG4;ur4S>tJ@N?+r@)HL|bJQD!yeB@A@G(%M z*M0Ptv3)2Y2=3VqG6}-R&G54>Ps(n;AGDquEEE(^_R8blsTrxac-X(-|w)bf=_(m`ow zaNg(!H2Vcc%hTRT&^dUouq%lVXtjpv=or5NmyrbKKk&sDmYt${dI-ua_il^-=8o{Bf^9fjdI%)N2MR1dA0S_ag*|o!`URdeo>O(MK zG_WjQ_(MiiH}c7E{BIo;%6UTK2mAwFD=FQsmeN;)!>WUC?p&>h`?D0tUn_l$tg_)Y)*-CDC;FGOxNcAs{~P|DKT29~u7+O!0x^ots);jpiUEp2;_s z6MtvAjZVp;rXxOzgv?xr$=2e9~u~)TP70o1^gYkfmw8`8wsU| zxRNo%B{bu@4d|eOP}~+p;H$OhVFR?#q;rmDZ+zAPcrj#%xIC|kzeN6P5^<%*-P=T! z*G7dghZ;nG;PbXYbVB1gR=M#$K7UuN#xmL(gCk6HEW%OEeLYvT!%M)eZvOliT{=DR z!fg=}Vi46Fiqf4VE=STH-B;efy@O9E%*E~Ryjs8+yY_X!*X79XeaU(=OgmRw88?o| zc0b?|6e+M?zE5cvT*NyNX5*cbi2JEacJ0Xuqd5s>l+j$ontj%(|8FFAVd`p#``X&% z=; zjqPG#B67#DKsH%H889yBzIh>#;b&mcG#Eldpg)Ugl#ko_dJ0}-2UPq~0|r&&`kfVa zttBNhAvgu12}7a=LDzB_k-M(39YN<8n2`e3cnE6y7?;2`uCMIZUyWkr2R8bCW{g5Q zYmhoRDLwo*fJ8@uke&bO02~{-wiCnAj(q`t6fvh;wtCS0xV-8-*7wLe1@weKM=%NapLnX`Gavcm|Vo*p#COt8e$giV&ejhn;t(X z$k9i%%wam1PC11qQ0Q`2;l*l*b6gSJuEr|tJqyOP$E3z#gPcQ~P21sEl~K%NWYJpE zDd_DL`8$2!)1|?zXyUuF3Voe4a&09Wfs>FL(E3Rqt{lxGWnJ@+9n>A!ca5PlHAAO3T&n}qMl=hhG1J?eX z?=}SgMPdhb-W7EZgIcl$IG*rh9T3WqwxqgX;Y2L_FA1u}U>-O=8Y$0aD&ZX$2BSt# zkManF$Is3!&-C}M%m+b@?<;t+9KK}l<1EZ`!j;GPjxLlu){L!4(uN^;(Kt&`N~hRQ z$S7-|eqEDko`e&$I?0rp7-J2La$;#o|G|eR$94V2mO59X5eeX8?f_>`_Wx;x>TCBi zSj;~*fn?W!Ce?C1#ga+R&;Z~m47B~G2n>en40XrgA&gl2={g{swj4PrQ2}bpitdL? zPHzML66!?=E<9{S-!s-Ht`Xcv2KbHa@%bY8%HeEE3y>Z6R(eHko2)L9J2jwUIf&6U za^#g!kEWOwJMXOTClu<0DY(L}iW;&Vj7zbDdaRWqHAV%SLjuOfPO+^T&VHT2pa9(} zf@vrn(8f*uBTTs2V~Hh&1^tCvVpExMs9ULk^U7P?MN^Yn0a&R(nG^!H8MH91V)hdx zU`mQAW_!Y4ZVs`nAUU~KS2A&oZQ*wIJq()w9sORl3hC-{Z*rPwp#HN_q3gu}WV=XG z@t<6$q<3rgf5f9HUUJBVP=gD;(rQfH%}y4yb$V$1y66;YozK_qZcos1yTrJAvc4ZR zN>D+Ap>T~IVFF~Vf|8+S!6Smq!v(iiUko{w@jAjZ5nnKW=`ah}i<@0q3qU0mGpZ@4 z{P_i-y63Y~oQjkuhhaG|V+?%QE3y;b*53DbFMm$2BdG&&^Z~YTc+b;{!do_b;w^}- zK7P5zTaut&yhizPrHQ0h`y=1!Z*KPvSVEE0z2NyXo#tCp z#TR4Rij^-{pW*UgZ^8>!a&_;=-(j3(ElN8E%0Q3n33w7YJs?#9UF=%iCy0PCq)Cf1 zzj5z)x4WFF*s+IU9e^RT4fHvJa#T_pWlUaU$)`o`-}mSmn5pbmWNG^`kOa%&upf;S zshw6265CCedtE$e2dB9P(Jn)AB}=uSiteM1wA|h!0;d-KJ65+hc-zp&iKm@OXok#vYYiKMN^b>RL(CI( zgSQLQ<(s3kdxrOb=jNc=Kn}x6G>MCh*52W$NdM3Fv^fKvh@h)7G;ncx%+K&7|2Y+3ods#(V?Hwol8Wr1ZF|lH-%FP)e$CAvNZvO@ zhnC1Xi7l-+^7980(V=XYZ9JBXh{R`$E=!E4~BsP6yf z4E~XCzjXCx&Hhp}#&0KL+heBapr0WRAQV5Ndd)>KS*U-{&*GHY+@-(bApM3wko`6C zzo!@}{Ut4Yni{Gc8|-~t5O-ltyF(AJn8#4ASqZb9+&kq)>qcggP1d879m7Kx>&<}< zB6sxN-a-@Olqv}2;)IjJD_6{#&F-iGRKh+GUy1uPP6l7uea>F@r5|t?x>3GxtW)-S zy~ZAa{W^B$c3KVHzcWm?!G`v@*MM1dMT^*bZunWM=kA@m=pT$=`UQh-&&p>Wfnkh{$^x^n{*Bygu^$A*T}%&5_MJAS zLi}V8x%2XpmnYCpreVO#$|3ENV{5hDDKN(h#%lJ8>XiE^twNE3#9VpC_o$YS7_@KX z?UF8y=d@f{Q!Xj=$@*%>c|Z3nX$BKwRkcjr216mb3((^?JuV^8N`NV-`RK?%C#G9n zz}c9EZYYxKSDz8iuw<)AUjBT<_BsGH4KceI3ZEAS@rPBz!%7iC20UZ1uweL=s1~s^ zaFKiaO=uvzsTVw;n&oZrJT&{`th8I9;(>Zhl8m&;T4KXP9IVl)u`fzNso<+YB1zD; zAVFKxy|+Wj;|f}U=$3q<1?W6TO}At@XkG zqZqihe+>6$9!;+cq;S!A%zmCpZBZM^>tiWfh3%hYd>hS4gXI8K<)E@MKqU0}=gSS} z^DK+%+F_$-X(u&FWP!SPjq|BpU!Pp;r@|mKmUq@;+eZt7NevuSLxG2tRUE%hGyn}f zk`@P=0OSzPaRC1Xx-+M{VwL=2^jt4L^Hm*TiY>Y(yK$ut!XXg~?m}V9(Ew7j>>R^+ zub{*P>8{7eO0mpv{yQixi*h}KfOZ3w7&FCxO*#1HcDEaR0smtyF&q85iQwfc+Iv;$ zYDl{0=)%J-kq zjl2!DxQc|E^V%c{+1p`R!6jXoPoZQPx&L`@K=W~GWWX*o1B=-RMOW;G?z|2uM@+f! z37!(v#|zZu5U^&~@&2#W{DCi?g*}Lm83n8!H4g7AsR>5y0ZIL-m&IfsQWP;Y=}g*q3@bZ>uf#)%s-BiZ!-Ns`7G*)4L!B~VT1I@2U7YoKjMt5u9JBi4I*5z_FrLHxzAKuQd8vS)$g?#1$B7^BC# z9v5fm=OMDwlxiO}tiTHirii`mUs%u_NUfv)56qs0C&H9TJ21`O1zby`VN0JmY=H&huFMXIrhZgDFJ(LNFFJGpdcFVsrnKq z#9RV|?P^DI3W7k%mcb-f+A&q)Ent+3EXRWUUx2;lrh>8ePG^9C8Tic+p0?Jj+NAck zxdF>7C;c0Kz)>iw^fBluh`>5Ok=`RyW8Ubu+!C$F)S_23^-r=1`?c0oJ5K7qRGwAV zR&IYcU>*(^$dJ?xN%A@7TBa}k=Q<+O}uZt~!H&PHM!upfQ*X2MzruA&kpLGdIH{3F#miw-{4YECCnvqoI5U3g^}j8*u4MT-Gk#@OMmMb)hyD$V1Is zSi075m5WRYX3_5oq-{AkAu**@tVMDpt*QE$&)k(y*O^{{8k`(Dqnmk*Wr6g|sk5Ds zj)u?40)f!NKWqn#drWFHZ@*8UnxS9x^oB+J{Yz9jOQyHq8pG)SZLgWF=fgK;-$u>1 z5H0*1`NcP1g^7wh><$hfYvPF4f@d(X7`-EODLiD~_*LJ^guCxw)CBTTy=No9r8xou zrE1MtnuJa|86z*ie)uA5N3(yPC+znYPhW1f7A9HK33O%J;p6|Q9EA02=RU`C*Zp9n zFSG2W;{i6+s-2aK8e^qzPpr@eX1i;&^gJouzjkaKy(0e?Or=pl!wzDaEkEjsPjt@-d95 zXQ_XlRe&3SGc{EcNIecUS`9FVb~>+$>3Pcz@hk5a%B06UK!OVD4vgql#wf+_ghEaq zw|!|$cO=G1$*QYtFU2Ai5=4YtduHDdce~OUX=jYQsq}Z^wb%L2QG?0-J{o<(8y^Jy zs|W-tN};gW4siy<%tDQSEBH>8VD4knDSZ6XgozNHyhJ&NlUBL`1q1wz!tqmSjEuUu zP(;9;_ENxgZ~#$$PZr$k-xgC^YLy-#a7DU?9e2~3=tS_B3KV5T-mZTv6?k$a9w{8% zDEd9-CnbX3xuI%I90HHrEhmy)8A%g7JcS)Cry!$_{{yEl(nEgalqo=8*N&SlCsxsl zBsgX_NYXHK%JS2Ni~wCm^M9io7U-$hFUeIk=2S#eNDq^mW8z=#DY&i-&=dx+5wJJm zT4f=}$XZ?bTeY$rn_+n_(Jwc%o?1#zqT2N1r`%9%C2clEUcm1VK2baLv$u^LgV4UIpIArT*Of(DVzJmDSQDkC$C+MTnCwJsACIoO-1x+e#$I z?$`d8M@BCZ%fD9Q6Q5nujtF`W4hV%o*d~zjSCNX)4gU?0jY}?UILf+o4R8UCp5Zw* z=>6!7fDiP5(AN-+Jq(rayA8EdD?&d2FMrd-J0sr*!})qZi|YLxSxry!&c^2LWB^=BDevaglEt8zbDcc zDcCL)+7SqkkZXUkl={!lg)ChjU?jUO+DVIN@RX`>zN+9ykmkIpGoEd&63hS`pDI0u z#0$X-%!1@ZhOZz`=~OO;vgv?>l=MMK?;**Bawx!O`#q9g^*gf1Gr>b+?ldJcVFemn zCrMKnm8s)B1|l{=lpIssf9rv^Yt5745>fkiS{+*?oc0X-A7OXl)>atpi5hozDDEyT z6u071+_hNIws?RPcb7t;xVuXW1XA3Kdr5GI1P|WJIrGeY=AM}|_fN>q{=RSTz1I4@ zETEQ)tF^oX`uzgXF-)uHe*70eP*!)toMSK$d+9n0!z|M^YGXPy&Ha^vFvitoRgfEoKFH-|^LPq1)pm9F*hpTL!Q|que|#kNNL;4Zl~0?L;0w(ZFucQySC3vo@|&qQi@Z6 zX}v~RZV0zU+;=Q}sn)_AgB^;}9eh6d`Kt#-<=u+xAhcU5p5^NpkkODdwbVS1fOZ+u z1-k861OD3&fDr(K4>~~Guree8x^l}6hx)oOL#vq_evC1Fr7ahW9PZjty>w3X=2LwyT|~+R4R9F`K>?KuD~>LDOW02OrEOB-r?L& z^X`g#3?_El*#LUIe<3tRQu*`4!JMklI=p^fMC#BsW*@KUoH+6}D+ON}n%+W9RrWVj znyQFd)H5uA1Xck)nS{riQRmR^rgRoJf+{zP90_p zi)Q?#u}icAl(y7t6^{~s!)ZX-C1Y{GnhgEtFZiRV#=d+>iFfSt^|N^A-lRiDJqrmJ z`xR;u!8T$4v=-h)6h%T)pccoW>mIj`lACVk%u2#WUL3_UgWD(V#Z~;dfhj(NPSm&6 z2;Eb&$%YPNskcIM3?UC6eZ8cRQj@r+0LIcyZ#*ImHYI$s~^|Ld<`I(LpEBt>%wmCLc?B{xN3$i%fCxeQ`zDA zLVR~;@-3oPf@5?*Y^>JhJ_X;~nlhM6zE!)sLU)tqy)#4u@su%KT;-xsvWN#O+QdJ> zs$@D~KeaSQzs6)e5-ESGcap=dLgJ&M&$T+B^%C`z;}o?PtwAUa>>NvKx^ndgnxHw; z{Nv+fn$kB*MLM9TBR}~5R@~8I*m6lNR2M`GCJZ|mGO?TeU^Too@-13vfh7z@#HBw# zR%-JW4Mz0J$`~y#_lSSdfk+{c)IXM`EhuCuQAF1X4O!FRdlu1tW%Ew&{fUozcfEdN zyRp9$^DvE8WQR@6VLB{JQZY$fHg zd%fqiEal$C94eIV_Kd2EGUT9zhk()pu@&vOT1Ih4<|7HHa~Et=t2~L3P{LKpj8Rx? zo-^saDGnv&w^7y*bsnZ69#*U;H}{*Wh;>%IZCwgC6L={0+Nz$1P8%K3$i%I{{7~e$2mYL4*R>zwoAqNyD~^eLp6flIuk%}hS;~p4 zpsCBTHVO5gdqjbp7q0K#Jz2fRXGhZTkx9aIgE+QjRd{e$i`zAJN*obey{d{>75{yR z`pFo6Sccu(>%U*i$nCyRxAn4vnQqxqYQH9cBhXfW!#a>wSs?oX52aD zvA|sXBPWT@ACh5Z>C3m+m(c+4+~s}@^B#@Y5; zqHN-g@T~@Lxr|=Se|1KzrS;EFO9djM4`0g z4ae&GdjttmAGnEWY_Hp`zVz!4tfa(ojM;=pPGVeJ#fTVqs^^*@t`iLZ814(GGFRm@ zB>r&$2M%tE6Is=psk&KE)=>9JhQj!c=w&{&P;3nt zMll~%5Ww0np5w{6b6!S9t7xgBH<7d{kIQX9N=v^iZfl+pvucDh!H{(-PX)TAGL04Z z361vhYz&b2RRdTd=RkD~5==U#_+xUlCk90h`z^3UgT71pUFhw+55|GP>>c9?x6uSQ z>5rx>BBqkT0`UkXEUJLai`EbX`Qw({t2tmDBjnqbqXCMrI8Vr)Tr10W(GYQcB{eWaw*W#P~?&Fzx0k=n)YX%QVLFjm?gxB(E z@6VTcq3;xTDH$I-Im6HjXbb}{+B?okBEsTJ#q9M7wz>?Ako$~TQt4XD^| z5VcT>9HKEnIz8^Lz9S19cYITK7PF($kIwG&N_~8HS5Hjg7(gR^#CiqJ% z2Xn_F)OP|UNs-XwRxWWMP*W#*l+7k+#9cp*;}?r2=aMcX_7=CIW{>E#+UMV-;?{DF zM96<)wH_>dmmemF7vLve&sSNJt+w{o8O09ua$ud9d_tE-W^B<7;O_dZS?p_{M!`b(0@68iROE*`%hjx1GA0 zFBhu}bAd2VO>l^g+e<`@ad)_f-*P5-ar~&1{f;iet1h+EmcC1%7{5vZ#=?tZSSOfy zihluaFp#A@{AV8 z_!px${r4}hzK>X%I8;h`@4_BDTNgJsebk20aFzW`zwoXMq;BSEFJTZ;!B@Q6a9?h- z%D-$W8A-kX)uQ`c1c_e3<+DQ|Knbq?hz!tnugJ~}iVOX^1e;l$OcGr8u$No!2Wld% zM=X9LO}tdi!A%@GKAt3Llj^@067j{Z0Ox8>_K%EU{EFf`W<= ztD`{US7|2v6*$|w`0o*V=2EBK(_MchQ_Vf*A!J-7T`^JMIiGzk`+*G<5Ncz@r4@iu zc`z5VZTyl}FR1q$*+KY34|A7BD_J7!A+C=uVVlDD4T?o^bXA^|X6@h4hUbf2fLipl zzUhN}=e4Axa6o19lGlJ)syO|t&OH5#6_6JWYiERsY|`Wn#MXa3FH%A)fWcO5tOGXW zH=T#A0~sRx+Pj9GxlA{lNoN`@@f)=?Ls=K4?n+U?+8=7IWwAcL+Httc@QP=jA@lt3 z!o)$$z2Mxc+MPf+5%#X;LAsCN& zRUIQ4-i<)s$ZJ6w@>~Q=Q>F9Ot~{V%@*2zn{s`N7%LJ>HG@c6#*1ra*%!Mjwd-lxV z1e#g7K`7Defa?ae`!@8pU2lDf$r>#8ww}FtBa}G!0|fFA1dLj5*T2!!!I*yq?RQ(? z+ThfNQ(46BKaS#P*9JV=r}7@#z^@Q!b3~!V(Xnt6a35zEAlHhXoM5j?f^wtmdt~_C zQ>m9$9Z&#oQ(8>v>#THZg#D4v%ZL<3-4&f~m$f?#YenZK;pHw=UJ3+f(k6sA9{H!} zVQ1?j*hzHzQZAxN$6p;I(?l~W@#{36{l@$IH*mgr8|~c=Hx_MJ{uM#Vg?{tZz(H2u zna^No*-cU@-q|)(DbMu-lK1ggFayxV2RvYU{ z_OtpGEJqIn8Q`^fDv3AtJz$p5P;(MTYnEZrWRQxI7icLByTcpp;#sBSU5+ugmcIZw zI2FUWzqZ0U0s9g1>8wdQ{q}`&M6Y6EU;*=%EV%N9Sic$2n5z+)${(Q8EJjnl|Gsws zovkbo*EFo~Rs&08x&TH?rz#|!rs)<^u@@uBw#4}P)+S`YTFxnv_8)n@1@U| zxQLCQBcW0N-Al{%r6J@7@fJS22IOxmwyxTyh`1)%T*#88lsyRUuv_=I;Xg7tb?;`e3iIX4vFeCuZ#RwLTM$ma+I?VKykW>@QR$ zo2u8?obf?gb4{Fc3i@_O;GlHpPX-MS#WQ{``##$5hN~;(Zn?d)Pzv3V-X~c=&J*u8 zUNMO<1U8^1*`|n(=`b(G$Ls7(U=X~dJ)u0=nG6=SB#daR?yoWPbKc_;f~11X{JDd-~(hD`jkz4AKs)U|>O5SrVz&H%P=%=s-BHCd$;P9SzF%-V$xV0XSwGUn>0@frvG42 z=~L9ntfKqaP2v8&mLVz!3(B>xesYsgc4iavekf!A<8We}H<>J-?TuBI^6+1S_eI>u zd5&lF4Yd9t!V(``0>Tm5IGt!$P%oXZ+&Xl4nF&o}6No^uRCWVY!e)YiLg>Qz3MA(M zXeMWiT?Hqg@hQu8h@~Z>fy+^0S4iYf^1`9k`vE%Po69xcOimWz(g8w|T$BsCoz!0` zp5-bW9Lx=2s|OyC4_RTi1ZFS$VAsE1veq*hG!WHM4!syUbN6`YVz>1WAy6PmQGHF; z)J;~0C{uC(h(B3$*AWRe7XuzopI9yDKDw==cgTtYt&c$PAE4V_Hs1!MPEy%Ga7L4c z@jS{^lH_LB`;XbwKiJJTB_uN?KYt;n#~bo7EgyN%tSUU1_Qa3Hv`zUsnn9~LCUBSW zM$B->kcc`3cEK#%f5QOp&?i!JRqxn-lhlg>Zf?hqCk@%@ z2y`k6r!;kmOSYtCB95|VbUZTA^GLS@XtaCLx%TIaJ@Kz_yKbx#thUNnNj*mSWP3`f zx-=AYUY|hBDrxu_3owiMc?2`$1>$li1!l@vDK(_{*r>fIb^MEan4-YrBtl_>Fq~!m zo1_@;BZVRN65BNFX#l0}4Bob!1_c&Qh=ptzV?6oBPJ3vJs(o+7+R4E#7uOM(Y*SEK!pvM z`rsYnh%jM9_Mog999lvJ!~lnWX9S@53W1TOOR`kR+ab}mSk`iqH!(_FcMkmN|D)mKPAq!Ise>yW1+xSpe5wfgNx0_xf zgm`N>^~tg=dI}?K zikK%B^uLE%74J{}D0H#{mSGMUl{U|F)5!nmaLt6p5oU=tmMCCQ5DGq1Q z`L%}~qrNO}JchnsMg!`f-@y+-D09*pMn(w;EKO(?f#L|C4}v6|?fn2%*Kx=JT&RmD z|II`i^DVCKG#PWr4BK8k2n2jN;Bl3r?RSZ?X`5kcx1LvtTwJC`GOCn&*gEClwk&E?(V#Yb-aVk>qYF?NDVCdKizbn*-n$oHcwh+#;Gj4vyPQ zK`Rflnv%wUoyE@jEq<(OY~njn|L(t_EWA!k-z!bFHj_;VwYR248NBOU z#MknYe@o8`@C~AYi8GyPo3Gp{of=b; z#5Ac_UNZ3h(tV4E^M}aq-`2rhHkcV%%*aEAm=wVc*&v>ip!o{%!XT7+&s3n8SR%k) zD0UCI9gh;ZXp(JC&yw;o=O6@%PM?;&EJXUx(^G%%%4L0{Tjxl`9gg_uh_TMu9}nH! z^I_~Z-@Y_~Dn>V$C21tY{4xp%)*no!R~iYt$T9mBl~4sY>W&s8hlJM$0K)blsgwQ< zkn3U#I0$zNa%^}8>->g0sBXE{r{$)VsE!^~aAZn!4jEnuz9~(x(I-!m_N=X_vQknx zC&l;wm$Hcw;CZ0}Y>0J%Z*P3A@0ASogGZ~~wZ5b5afPa_0!6J`=u+|izAN`Ey{b{( zA-baQ*I$9iCyh*YZ^;)cU?w0E0CzI$O+Yp0H17k@9zTOG5JI@)ndo*r#@>DvD?t$~ zWk2B&NUEEV`1jjA@^OU_BIO=={Z}cJRBI%`e-*JosQj-7mOpF}i*=WYL?^9Tfq&ls zR0(+&T;$}Gz>T_9Peg}H-93yk&z10Vz5={ieM5xFqgV%z@ybxb1o6<)_(>RvpsO0} z@NUT=BEcnZ@~`-n=h@)af`Hpzd|x|VEv%AjkGzlN~?jt9(=GJ6qOq|P2r4dzEvOT1?Gk!xl6Dt zb(?=0>D@0j>YDR2MST;qheHv}2oYI@kP!lw32Os5m``B_0F=&r0LZhw=1(5Cr4?c> z^DJ|zuB)EJ3~wc?yI$0If}QMZR+PP%UP|cbM;;A_ZX&!sSz6vM;T}Q1oLmtWsG~GTr;n z$ecf@@}MxJ5{iKcQ9sdD8ID^$1$0dB`p62p4)znx22ebHTJZl9 zec$72;&qrxu)W<@@Ylp*s4;t{{3!mAgtDg6?p-PY!13r~3~Fy<{V9nc+3&=@hgk;8 z@*69jE<(Zj?m#wEWng@Ez2~)cQ0ib4{E39p+>p&`((30j`m5XRaxQ zY}j1myN~o^^@WV>Nd;4}Ow!- zEXJR#MyenN4YseyCJMVyyJL}^itfyV{hDitXXhitSXl{BDQ85eT(%?E`tYOvguVpw z2TFNgFUSW3;I9Bu%ZUP~)-0_NRrUgJY%{XiB(#+XN^4r)ioIPD)!ym@X$06RSA$1+ zA5bQT4v#lQvDdwOkL9XMlGwl06|GL1;Dq)9&ms_d6E~Zyg9NltX=}76AGBY3x+vXL zJy5Qi_Xq{K3BH&1L^#PV(U%xme?%9#C?y|uBakpW6=1A-;?mCG0Kyb$Jxn?Pp~L@U zz@g=(eg941y_dw5x-#b7&yLHtzZM`hv3=h~Z-P5kBu!9?6Vj%{e^TdRNLVv#e7J8B z0vJvsLxYQ-WiW|iA48o=JHT9RU4(G5s;cxqG#oha-tBe;(Xn z22wJOXKG#NVM;7Mz|y=ZWd;Ae{bn7DT3_=Z!c%=%pI-L*XM+l1xH>Al-;8JCldSO5 zj6o!j8a)YqrWtdMv>m{E|F{vbrhuSlTzDw-px@zvCNk+;utQ}(Op$l(<$Yk?-0+Vn z`qTXYGXZim?&QTbuK|$zXr_5~a^iqE%E}1{MP5J0pmhKT5z}IKO9(mD0{Bhw8|d$k zu*v|JA8m;vpM3U@u;`?wVRhucV}6=^+z8&j&6|?dq3>Jcw^HM}AV7Uy9pw^ZeTTdp;UFym(zD z+6wWxb-r*dk7-Ay@UQKeOiBiY5@4pDqKi~w0RX?U^7e%@A3)a+JKhBMWMSjSKOFMM zH$H~#;#b=xh1Bt|a}5{bi=sT;-DB2{_e>sB*w&<%h)-OHy6p&c#tG2}io4p{p?vTz=<1OW%% zy)NiOR7&UvdOsSSTg931YGb(ZfTMnQkcpU=i+YwYBMDF53UN-r}PcH>mW4$ z!iYf(j#K5byT+UJChCiP>(lNTWpir-5HRz)A<1kOg8@iPGr6cM>F3oowm`?qj^|(^ zIbL>W-&py}@}tKY*TdN$wZvly+cOw#sI{PZ)Cx%cYaGPiDM%}cXa);jnY8T<0XhPP zLXhXXU15?_Ez^Mf^K=0Dd^6YD4&~mjhTYbD{*A zw+6upJ_YR;D_u58nevn5Em-lt4mHq zkQ%rzoHx#?3CKJLT^6qso{UgIK_EN1{JfYCKNF(als8E7Bp+bX(BF(XFVB;dIyFPp>P$bxjqM#o z)_)DMa{^#^LMctUD4qDO>5RL9IJ%hpvH(5Blln$rKk>;~MhdrA#!wAv_?wZAD)+F7 zS4{o(PrbZmTD6#m=mpx2xO_6`XT6#A20sk*GoZ=SQiLGcd^1l`yz=3iUUo=DeH zeGgg%zUSokvyL%+fT5DhMk<3lf|0g0oI!qsKZ~U@HxYZ=U>(KHUb2DzqXl2Q zgw-xj(XpAu$=FAVERqEl|0DQJCL*(&P&0_Di!$jwctWR!_9ZhK-Of5)_8E!nUn@eW zL5!-)%UxGheLaIQa$V7xs2h?vqGRw!ac7*+7x1+VU?|F3NUJRQ(2+Wy(f8zjO$1^mf?vTKC%UKce-!WikG#gmcF50 z>egv2>=p9EtQ1km?AADjb_UMasd<0=oUX` z?troB=0BemJ(h@~0NMxw_$h=K0^(qJr?Q@RMlZ-$1ECCcoy;@BhFSxhwQSDO=Wu&2 zljv!(50KCFF)Z|`xrg<8OT`Q+MH^n_jEK$KHmtk;0>}}^tdLTEbbIsqA!Vln*OP|n zV1+gwD~dL81t_qTjbJpnv7#VVWJ19##>9sUY5wY_yh3z8kn;L9(9mi3PLf!AyKr#( z?xSDur0O!1kjTwInq%U;0~!EUGaz$!W)=3Vm3m14VkywywSpL@GaG`sO+W+c3TGDf z(Cjbqr(ms2^f&yAbKsaDR=E+bh{fPcG}FLb$J%YcS^i*AYo%bLdr_L8SOZ0-QjE1_ ziHEmK-tt3O*4Vm1;T68ojgU)ZGI5vFpb z?ws6lsO1C8<^${K=MKD=udLr1fYDe&pM+beyYx(EJ4*^rSQJ&$e%1o&eUwrjk+SGj zgrCNeF~gKD%j~*jeGJqqnHixDMSHg>;&dDDu&Z&>J{_q#mse1~PMxFk>KyXqKh=X* zZNmf@AN%Uq8_2(A)&R3Z!u%k!B3VP({j5lJT<+=2Na+c!Y=x3(%nogmQpdVn`48Uo zF7<3@7Iw|#I;R?N74tW_OU{bXvBQ!+<9}c1MZ*ISr|h|0eFr#N2ds)VX2m_JqZU_qGbaOz8H5WsX~NM`IKaD2wNi&j&XM zL{8#3DOAthb>Jc~$yOJS(<5rwc#K^L<#dWef2xe}ei1O6dtJV0W# zjG#5cd7aI2t<4{vLfOJCst*pf4C(g=_*B8Q^ifV^G2DBIn-G01GO?{Y^z3G;CRKi@ zbDqO$E%g0+L>4z{7f(T-jm*&BofuW}wy?g-?+k>s@|ff*jPJ>41&f1-s2Qg}@G1Yi z5!{X6j}d9w4)?HTFT9Y8h)GiNIO@C3sWPGar?j~lYk6x2KC;-5y8MkKRPx}S&Bz>WEPEYfl z%Bv90EX!v(Jkt1D^HY}Z4&yg+Ry17=S6AOGLcA5RFKi+;Id{%0LIt;J$Z#EaL0Aua zg2BP8_m@JcpHxaz7KfDAX`x|$TfWIBqzm8gik_olE_Si5Cs*NJgw4r~^ex_mt&ob` z1YTPV)TB?!aly_d0RqNx1NE8Bq&OJ9ykAD+mxR-yW#gn#ExHV<}>uJ$h}qTA|i^qxz#1t|YHkSxtvc!)jGv3N(qG{# zcP=)~SsdmwZbvxLn7KrZ6b#Pl5=!g2X=#jY7zDs&HD?_baw4|vw?!f$-?hn$aL9X4*=|cfDcasn~PgoIBOoc4pjX;UpS4% zE}Hu4Kz<f=$cK#eaI*c?iF!52SUcY~o={2Gya6KR@nqZQL;91MexZv$K(uy2@rwa$4ZzUz@!PTw zx%DiLPZI<0DY&Wm%$NC4hV+@!YS@c%k|mtL>Uzsrtb`E=Z5%HI8(>)5+IPwf<48<7^g}z0euPPDjmbG<&f&>s&mTzX5Ur8F z6?^vV{4!Kf%20|t&?My)P%6w_l2%>PeAlF`HQYrzEZ{rKl7qiaNy~ri%SA2Z8oP7; zI<1ZJ$DL!e+!xb}wEez^{m2+L0N`waO((1>xFAeTXAu=U6<1G*_R=;uSzMLP!`4$k zf36I3{y0i_*N54|Q8QQInCO(KK3ZjS6xCTiLsu3bD*GlJ_HaV*->VR;V#)s}6(T98 zRq{Vo2u7b=j^2miC=D3wf1x1+4vPM7G{n^fBi+xGR#X9s-*Q)-pHmFgf={5blIc^Y zKSABgU~5*VP8e6RjNH3wHI_A}!E5GVGE z*}+P(DZYo&)SZJ(=u;#aQfQp^%08rIGfzm+@LHu=&ie33w;@e1E)~->O$?B-M0<{M z%ypK8&KPs*TTv~!vyn1I;mG2x<3lH6xqM$lxEXzzT3zQSmQ5ShjXe@)7*&tyNS*;O z(-`gDck{4R+2}*jRdw@*Ty8vFejEGL4&p^i#Pj5o8KMFiHGFp4QDZ3~HLQog?0vKXj_}z=FNGgJHy6n)yUcvaeJb_qT+vfsS ztO;-Kve}2R`4sb~pjHR{6>03-0r6}S>8gwg3Fuk`Z=@}TeQ-LG|GCSe5GJsfNB2Gu zG%f`uLWH6!aX2a97)eY#shl^FYF+-(1fof$i|*2s&Ttf2uT zq=y8cwsfrDd%9;}?3z;os7y8(A)VhJc`bA&$Mbo55aS=>=Z*^cT$@7BMLG_{|zVc zgYfGA%SqVwwWE61rrM}6rv*JijhPl#Arh-GhcTsxMW6G110*zfdf#}3y}_QB?wm9< zEtNs(%$bdh(w7cP_cpsd>x_FG;X8imHxOG!f(gxCV8R2b+gD(!%_yjKO2CB8brE{Y zOq$>KT}b6>gpE^}efy3(oz0n^ce`qYVPjZNUyr`fJ6 zRC*rXr9*x#TW&&aNy^lQi@8!AvawLfP<6#|cD`c#tiu%OTe!x|Y}HdvV3k(3psh%Z zA_r$i>St&Pck3Jb5H9N3pMH==hyJo@g3$e6y@?%?|Hhjrfc1CK_y5V2{R)5R+0X-- z7kA=>;RVTeYUiu0UpK_~98jvL5dC%v1JI~^Y=o5}3qQ6>d6(u(^2t7SkrvSk0FAV; zpT8FVdg&*OrWPjt%C`I;>csZ`X&_rxr$8%cb{wZVDI|RNoRv1gJ6q1l6|rF<)GzW* z{@}87S;z~0%!`a2=cR~aqL5kxo-HR;<2v#K&3hezpz|V zNVr5Ack*SY7PB(LTJ@p^f!y=INsIrKRXh%;jIBzLW@Ngu7_5kFpBXiH=OEdYfC?i% z1CtFXgUm(7y;U~E_?TITusS-wx@G;kI?2YCEY|UGD3=_I70CfeIF^ZxN==s2#HDo% zr6A8#Dkd`1=GyliQ=fsUs~%)P8aGDfzw7I4G;Yzi$_)1GwEs~&*ooNToAX7r)EwRX zWB}$4tC<(RY5;!s5tpkMYsvAR3`@PRYW&D8e?=Z$07i5>0scLLH1s@8ED5$W{?Mf; z5aT5T8yj7~g>vn+Cj-%e2JE+C-aRwdN{GTZ^DJmm#k|6pA=<7P5{HwbY{7OUX#rQq zBx>*#R5V=Ul%JT7tu8zA-?c~fSli7!VOYa4^n%rua8qTID(x4|&(i1CbV&VY41yY} z_c+lC$ghEA5pfa^XCfS@l|&pmK?l%ZECB0|0Afz6*(HOjxJ!zPeY_t32sIXk7Ez+G zD;4JjYjHKYSCwKeAPwfeojFbhbjf|1x7;&6bITj?lb3nf8t2;xFlEyW%;>)m6?pGD z|4&3kr(*tpAu0&S2ZsHppI&82QrMM2$f@#FsNT`cP3nq#ILBSa@@f%PQ`jjAz&=<@ zy+s=!cx*cS-<=A|7pKBX^am@@^69J_t_%Z@!z#D^B|q#7BfzvBMmEb`>8Ps;F>ZK7 z5^guPP^n~Ln3snxH;Rj1swWy5TJNPeo5-iCDV`s>_{hc7YQb8XQ3b7tQ>?#%8IV7G z!#-{DPQ-n*f~LsK+u~%qC%Z_;T1ED2e>8yZ0~|w_%cyX=c1qc;67x?BlvK^{^sp-R zUd#$m{<=2wA%Li94i=|PYE*Kl=84UGk-9{Z`1zd>ECV~#O&2#~;T#>AwzSkGr_I(- zq9b(_7EzFPi@RvRGD4aYe$I_z;-GyNul2A02Fy?gD+mHm^F?U(WMJh3(#0ewKTL(I z+|Q72a>DYA)nSx2x+qknuAxS$?80gQX-O zXiFmcZ04T)^j22oao7+=92HeoneWsSZ<{PVoGn>Wkz|HDjQHqQ3w~gM8fUBFzhUhH zKW>_W$#@?YMRu>4(+ejW##SH#S9Q}C!MqlK(ELkSN<-U)|GCzP{ikoiZb9fPtBte_-CNErO21*e)Hn{?B+t@+Zr+r9I&PPF|cw->tV`Tw2AwJcN?%z=cY$ zV#Wk+Yd_M$KFWRFK|c7fzAxQ(b;g=!9)+GNwEhKgodN@%XpPLOhHd4f{ZjE~q>qo= zmZn+krqh;J1EvrmjGX@g_)rk4F9AL@9EW!fBAj|)MeSx_{i#Qn$M`RQ$^Z)Ra#937 zZ~Wow&%Yjmcw0ecDEh~WnQuFi8z3~^s`ZZ3aJYdnd*Hs3=h)<3ZVMwziEcH+#t&*) zced-X4^1!qD$#@^Uydg=&zliVnartVPR21_zadR@moNrD!2io*I9%Mu)e0`9S281g z>yeOV?x}Up-s*H|W%lr}s&6L@$dj9JLOZTp)RPs&8VdYfD=Bl{)xE%ST^!(V-?Zlf z`WMJ^vy#`h@)L8|EUCB1jK=}zLqxS>a~G); zxm7$GJy}DEc)&R$qUd+o^0EG6VPtr%j#TxT+ga6lJy>GCCp>p57q=M86`Jk1P@@}&l`4w>Lsnk;M@{(H?PP8xs9D>WVZocLjsWbk`>+V|Obmsd zisYP+-|2cj(pJ-}_Dg zKh5b7jD0=e--l-Qs9VaQhL;s$hupmufkwwh?n>Ukllmm{qXUjhU(t95$YRwE#Fdfh zpY~&=pP)~_B;KeggCUT}%3?-f=X;Rf+7S%Yn0Ed!iNbmGNqWF)o=LI?;^7qZzls~b z4*o;jfPS?P!cl&iUS1!bzJH@OILEm3d25VtYJ_;WJ$bw=@8bC{pyAI-Frzy+>jv&k z)a@_tfX@}52Mm4Jlh*>jK|&Y&iW1UU%7^`H4~i4mhdoqRb!sQhq=>$RgdZeDhN&na zIX7A4N`&2qny~K9yMi!E(r(&i*IMAwQ6_;VS2z+uv+*j+iXk zkaJ4SfGT{tTk>8vUopr0HL)Rq55&cFw>;QaTq8m(JtbQlpVYI&P#;Jr%$nTOiSBG| z;Odpprtekyacudy@!68HwmpES%*^|cQuYokkNlB#(mdcm-QWZ9l4QrvO*4H9 zl<1!3j#N~r9s2Jg$xiyh%nnyCs`J~HVeHLLKFbTzl)UVz(V9$t{zl0r`hnLV)GMlY zEnd|%($SYD*@WuA*g|EsG1#vX3Mq+WnK_*@qr6~scj1q9X@pooYJeu~sJ>Oh`WM^- z5KWT;A2V87p?heWgwZ5wV3WJ}t1OH~Jf|FLaXNn|SVBK^BZ1Xrama*i-RE}Oc(zyN zuKBG8>Ph2U4+|VvvH$kz!^-^yXdL@a+h(KTIWdgkR|JVYU3LVhWwF(fAU+@^vX4g2 z;w6=LmmvjClkjbrZ;FNw@AD7s=2&15PAF$1@ZFv*LUZ-n`Tn3P4f9M9E)7a#QmKRz zBdBmoKD))Q8{GHKp*urG8}Gc(J=HgV3KOQ^ukCiU(=6(=?%V5wY0s{gkAO2Zm(A5^ z`Gz1Fq(f;eGD(SysB5uVY2U;XFztaE(9r?%hJsH6*HQ03MUnf0o4mXlWySXjZilE- z<@%}aZ@WlQ!|*jHU zR%qF9BQwkr@CWeql$aR%xzDHeArhfw#`&yZM+yiL`mw?J*Z=Ep1Z4gvj{0JDe;mG8 zlB1CS7ax}&d>=0i&Yy~wm$n}OVCL~x>pQMrP(4rnA-7R5p5>4jZT{;)Y^fqVvjX)k zoZy@;OdaebtQcmkXLAyC+CebM|IkGw>(c19(c89!{IfIt(2{aXQ-UH*zHQ+yrlrEL ziNK+^^MlQOKTEfK1C;zA6^tIzdk_cN6vd^#k`GOUN3F;H4dmn51XS+d*F(;UnlM52bU%fuj3dw{@hJWwE{Q1(#&Hu4hl1f7slT3;*q0B%L z`K`DA9bwI|zee)&JfQmF@1wq%YZQVG<;!!o53%BR`(f86iwVQ_?j1(A-xb?(hM47hqX~G+BMr_;n4iM8c7rAz414wFS`0VW?N`Qon{fi&!q%#S8e~ z@t!{@Xo0kgwhM56xniVPSt@o(*AB111cWWz;EnHOL&}ikhhn9~#B$9*-1hH$Ro=b? zZvJmV$v?KbBZ=RfgB2pcfSz<}pVQ|ebO_fk*O0;9Xc+8$be zxXHkUub-Y(l)R=Fx6~fJ%-(mxe7`1KNQ)>53~-nt9WiRQJXljck5FzK0K#X`5d?$N znFEu4-2Qd{shbwSp&_Z6A;(puiX&!C(BP?9zLvYaUCuf3@#_A3IG{o1(7*H3kYn3<)GSHosw zyjB&f$|Z~UXfaQS*6iE{o$T&1wvwHbItEUSOv>M;Uw^1aUy*$p?vkwU2C%wv#;%Ao zT&T9`bm)=x%5Lz=u96y8Hf4`8!J{opj^nC7!v+5?1l9)Y)Xd!Q|GfE&T8inm@Pz}` z3J*R|uZOmzSp}LA(jUMWtunAWhoD@IY?5{?jA`_ej)#U5tbLOVo=wT?_IEauLhF6_ zDai9`Y9i4YF#N4xeb=RjLCV6+1BG3Oo`iA~?&uWE4=A688-dTc{9wQ$>My`!z+3%Z z&mNOzUu5PPxR3_HlJ4W+5yJ3gbLFM$*`XYIK;$mYXs-UQT;vT5_nB3^wPp{n6Z zkGVb20%CBqWwX@E;^qJ3Qrv!R#URyQG+;likaV-JjLX27F>1TJEfzvC`x~IezvUh zS;)Z}I3Lr7?=WrrO5e{Ih{CJC7WgVp;LX7g0@$Z6&YFHJTd!Em;%Xu(M?3vA6~j9$ zi)Aww7r~D14=K{q$E?CIh=aB56)YTO`|uINQSKls=9?pFTT;!B`65=q+YkR^sqYyW z@Bir4X29E*m-jH%`at9fsEgjzQH6T65ErB=1+*BHM|u^GL#2|$iZ2r)n>@me!036@7??ThG;GdSE;E^? zHl!*v0X>$OLFT5cn={Kat}*_ZNiqgdCS{I(nLs$J4M7?@0qm-e<6LGIvvjiqqY--! zZ$-xAMv83`jf2;mlS>Na!34qI4*VQu#mFADU9V*suOu4wD};k8{47;e5Z|AEd>QI` zIfMz4OR87Sz1p|5q%yv@u)DN@`@u_8r_6?ZG{ z4n+$s?h+tCfRAsTP)eoQO zY5R`4ldZ`V-FtLsQaRVPinB+2^?z!8{Dos8istK1e)&|v?>9^#h>6c)*9YZA1$(@(&C>=Q1)R9FQ6$81i9N z-`H83rv?DE`bQS>7H`bA;Dvtwp!2(fVV=@)fYLR{4Afz2bRR5QpwyoZ8}K>ten?H2 zH0+MBqLFqfr*|wt%j^TvBiYI3sW>}RkIANftSwbfZ)x1t)t(6UQyKQFTH*>S@8@=S z%(hM!pyId4PQ6?Dbpcs4{vb`IAf?|iYh82^LzPV$2QZD@pC;1s(iYw`{q=hB*zzx8 z5u@Fb$5E<_8bTKl`9rn~0vwj@Ra0LK!*8c=xq^m%CdWmj#%UTWeVV`$A*5azaT(JQ z{e>^qHF%#WU;QEwD@Cpo?Tp9K!>DF3Xa0HjwOynXOTfE^eM=tY<(R}HJ{Vo;fJimc zRW@Na(|=uWMr%%EAW{nL;1|L`9&NzP0N_J?&ONC~u)`)$~1BOU;wIY-H018r}!-GsM%vWV*Wk>(=Of~oyAwA)ELsyMd| zaoJDuFBa(Lx0C@rzDl)bjxd2iMxvh&BiduRdW~#`07o_fdp8yJj~8)oafvN&PGeIy z)`sorgbPXBrll+XrtXnPA_d#ZJ&*6csE8*~du;Q9kZhYLyqgIQ`PnVa`(g9fFebpv zP=QLmby36{4n42*8fNLUX|tkN)FzQ^N&aTvLE3o000+?Np?|4xupZl7%%`1bBkydB ze#}<@$TRdGDX=XWh-o72K#<*;H+PAbG2o-G%)h>QG#8jlsqu%5tc65V-uT81NeYCH zEspATat3ZypKvQ^$)g;98(X_|{n3uSU;v+p^c+(d#a{a;4*#(zalmYBT3gQn&@Sf~ zNyOnJOVBehWJ)HIilzLMSps2mY=S}D zLP=~Q!7mS1JS=FmWIu>7KB;jKz03}_k2lCLke>~gJe_-NH&t^&^F?goX@gX60#|d) zuyj!rK3K-1ui}^o!j9#$UKccW?ijN+(9D!drbK+fO#80X6VB#`{$cSs<6qaK!0J-X zrat?@g952ba3{v2cQpZM*mM-WDNgrnOQXMjgA6X$Th5@OY?Y$U*!B5g9tFim{ zK)dIkcp|RW+>C0WDdssZMiky}B^B6w8Oct~aJZ~071#Gv7k~wEdyLeAbe*m_n>xJQ zwqkG3eGkotKLQ|q0_m7|{HM<^fp#Z!pCxZT_(MZPgho|oQVsb;m>92SI}+s4O0VQN zR1P9DSj-~MGvp((K)QI4V3oRm5S}4Fho>-QRE33op5e_BJ`ZtCq;G)$#EQ4?VW_fc-i0QgYyD^9cNv~H~^C#t1Iesq=t7%~&PUy#xMAni^GPqMkjpkW|r$?s?+ zT6^1A*EE0Z$D|e|@G@8zBF^&n7gHVVinWshyG=Z1AO5mXZXFG%l0FcU2d0G%;4}ycozfouV-ixV_mN~xMI%dp{yjgZL`3+I zXpK&Pxyj1$HO=(toflg8XV0Dom@Y@-rcY%IZ zX0no&8olWoVz=B{TMc0QDo0!I{?>n4ueEDDRC|V~OYRB7N#c{~#iT=>M8uX44YL}s z>&(cqi!v|d6z9)#Xbpx(-UH=vcB!*YB2Ej`~;iGmA;LBc$#57OAJmZcE)@%EV8Xs}*W-kDx4*|)b? zXZj=7AJN=t6O#m=TZB?VIxZyS^mS8@+0^9{D9}|2hw!^Yqgpl~2zz-ry9tI#GM*Nu^g z$Vn3s7DRnulV)a*U7aZjmVTaU-cW{Dz-VRsXtGFV(XnplIT|cqrt`N?J=fO7#o2t@ z@vldL2cZsnbKPz{o58$@JH$R=G9e<<06t@;?5>_GGCVc<0dJBtVV_8s(jXTuQ~*Q{ z_%b8O20|0A(rOZ5MIfI%L*&}zh!Sx=go4!}pd`OAns-EUYridX#;)|72~v#l1=(+b z1=Q}0$dZOkcCJQ0&OY}nyct-9IKW=8_5X~fh97E{hC&LHDK;wP4TH)vd}+Q{M<)xg zu+xl8+?2M!WuIBzO}Clj6W~;*zl$haE1l^#vbk2fQR1Fx2Q5SY*(AR)fBlQ}8Q-xg z@{{5F*sr++IW#G?lq|8H0gd*I_hC8<^5*}%S0P0o+Us@edeeo&lsA^$c`f8p*BYgL zX!j?WsYSxRb@ks&{43?%+0X=v+PU`!O42}OLOy-?C7)736vB3JNI?vE4(8!{cR3TR4`|$ z4V`IzxLB55jzRPF=w~yIP>?L*0F6WP$j6uMHqLJG91W9c-4-tD)Bd0~(_fxiHUo0h zXU{9~*)&c!)Q60r_NggpbGwq4Z9+)(PX5gr#-?8~eTiy)9|v?EFHNB5aaL83rNsLV z!AnZ!6I;Kv?7Za%6ws#FjJahCK>O7RsX9|XD}TJfn#0prlJMB%IVQt)EODJsII-=C zR=ny@n09?4cOPGB3P>)-7qbNWa~>DVzAi0Htprs1FPZ5<5@Rg)wfA89X|_d?+@y^4 zN-&X0%a+m~nyHHv05d!Tqh!ACY^0y6w*Jvr?SLaLf_xm3E$Cg90Xx^2{*x1zTSiit zi^?0#vxuxNpjx&8zSrJ?RC~IW`#kc4xU=Vs@S0K**xlh$A4Gp}G^wwj^EYDE9avQv zl=vZh4I5n_VQ@>0~Jv*|(gOgR4;@%}8WlUf~ZLWzapd#aRWrc7qB# zmVxa>hwPM1@5X<_ZA+&Oaz12&1V^zyJAVYHVn+ZN2VngXw|Ap$&r!>uC0eA!@{N>a zG^;K`&KO0#fN3ac>XTpgRq5Ac4Zp2#PRh)OejU*ZvD?6q&(@HU% zzG&xmdh*SlcfsH5DNC9!QGTw`#{RVA7{^Qkjgfs@lF!KlOAny+9OH-=ynjP5^IJS#BPwzVE;)F;f3Cyu)4vr11F$*(^{1eD(@n`sdHWAj zNGI$_Qw(;ilYS?Y5*cGQb+{{!K_1mp%ph$;L?FT1zu+}uK|qC*hVF@;xkTRaWGevL zq6R=x!FpfgPa3{pzo(t;p^H@nkQj6+k^`l+*=+H_dBknY+BgcrlcL~FXm4E7!8XFnXvsaLNz3^8( zkcd~DCn@;Dv`&1D^Fs&I(#TJCT2LY9)5C>R+;eShlS)$vaXP!D5DQW z<(9)C8jpp_513qGA@;AWweCz|MsTzqlHJvga{V;Hp7NX&{NnpYM?5kLC19S7f_Wtcf zeMA?WR!NxLjQ;T*rfZy7aLU@hp9HT?e7jRCrZ?KI2JsD4H%)#!n=hsM0&XwD>aH*Z zpTh{)z&kLnW1f|Z%hQfnFw+eC$V+>XO8Ex(&K{NCUz6Nsg0E_-vB+p1T#m!RT84M+iSKDRkC@n^Rc*yU-cFE&u=&O@o}mS(`NjsZTU z8=XU&8>F?4-g26mK^(D-LT9W!yAxF?OLcCe`>B&vak*p@h2Pvrw5h?zjHmk^aC$|$ z>O%=E)p3VxDfJ{Z^Snkc(?9od&eNL@T1-=E95z{1>2R&b&0y|;y;y4SRM6s`Ik6<~ z^Q%c;dvvhT`!JT2hl_CcVxtB=TZ7sXO|oEDi8LK6LG-yetIy21ebur zv&l8t?De_YL;Ku_qO^@4tM9-O%cG8<7_0m zEhF(iON7dnw8tdDVPO+pAX}KSr6N*K8^A+Ax6TGZpfQgoJj<`MX+^2i@`+8qbdT2%xV^0p0==3Q? zZsx`YYTgn9o|Uxcji$SJ%f$%AwY^}46vplmzcE(}Gx9Mnt!n>I;O$Rq5v)UVtI|-` zSvQ=E0mXxz*Ns^`2RJC?gHHtaC-<)x8pS=3L)V?{$;%X}=yP^UTHP@tZ!_{tj!FrV z^hxj{D6v|X_8ZtsmH0-UHi7sf(++Ezsddb)semTRmk75`mrz=%5d_aYb(teyOzUqf zlf5x7ny|My;-l*pZq`C7qemkyMLa3nH)SVaHA;a+N%H8i~fpS`X0oU zui%|{^F!qWvWXheSt1M)0}VBIXpYI$O|-VwKi@gp5FfVVg~fBtaXxB_B(`HwUMkp} zle$unQ651g6?K|)EGJ?&*ud}ShBw{w(`C-idZ1{AlXFJ|4dbhyWj0z9D;z9@Cq|)6WHEf9z3|0~M<@a6{qG zB%NxrHO`kxr#DmpNIZD<@m9+;ru|grrp;9{j`A2j`XeepJ>^V-dl#dc&NS|#jpQBn zi!y7myvVxPZ`SWz@YyP=ThA(GqwjSUNwvXY!U}$!59P0I7U?~uN z%jM-eU9?v>pZ*J3Y6OHtzZEj66+%W0X{ypIt|C3o)B3PYE9zTHOq~b>3K2L9CdrKtdSZA%P3i?V|6$I)eZr+-YO;Imc)oYgw#}amj<%q{Q-YDOvSY0_N$#v%?uC3 z7!!5nq}QjdAFYk!pNzI&zN1=srV_=mBxOta!(wZ+QThYHN(Gi&E8SWh!sUw-o<)4t zc^(lw4JHI)C%>rIVW}+Rr~6yI^Oy;&8e}Gz!d~zAxk^JQQ)Cm(;qpzglwo>>^4z?8 z=|%>Bs?IJweN)pZ&Ly5F1Htn4%VFvoeaqIQHzWc)i@fEh&B10L>fjbT5K?OiDK+Yo zl8kcwSSXe&ESr|z2RV+gn5SJXV!o1;ZA7wLb_Lk^%zcj!(6F9^D zGq@osZiuxeQleU8rp!IIr17-);dw+}qtMjh(@T5AhO`5_DUM?DO|?-!CFVd7Z*UiY z<0dE!vBUeJB=ldXjPSGTaFMtN9x&n_o8($F?dii6pQ~+OMql)lBD-Z+`vpJ-mueF06uddGNh{Qs6QVzY||5dWb#F{<~~fEK~~n z%5Z=2ZHu%;-q|nbg+K6+;%HC~|5{_4rMm19)k1h4W3wU*06e(|@AqFHU0{4{Pf{cm zg$u1E{i65g;pz;6k;$TvHmRvAi*~+B7>SLeyyJ1=Tp0X>XL9tft7&?JGM?C~VpSW)(mYiQo_wx%1I3AU!Z2bod)m-)f z8a3sQ1Q{SrnL4d3o%g(7qs&m_ehuygY9!7bL%*eR?X>hVDPM|%lin?=1N3`LuJ54B zF)d+XiiXGpsx6<~Aqg6!a)3$JVS(i_KYftcVqC)DWTBe87R!m_tdkpoT*hMSmsq!6 zgKL6KF2P1<6RnV6Tkj6(gpm0-PgeV)c+9VXfwMM?PHSq}EUvsLlLa@A_=si%5%R{=T1fiClY1|ZP8&i+ELm6Kk` zqVsSbkS1#@5DqnwUGPUzUD5RZ56J4lenFkdcyoM~7FO2{PB5~mn9DDAUW@#U8|q#y@s^iiOJ{oV+dlq!1mO~$DPdpB zj+CSS@k!XrV$!8Mwj51@rCpsnU-4kNK3QKrOo74E^Ob{%r@~0TKmbs^2@~owQtLBa zLQ+@ZE8Qa19|dfte{K0;zj%g92NmW8_V5pUk1Q=7x%BPuW@7BOaJ}L0Njui@O~5(I z|L5B-Jp-n-{D*o!0@e%3NrGZq8`W*b>u@T^)6eAU;7n~-qGX~^(^OOjw<;xSc(g`z z*FU>`F{C2|;V+mb3k3Ugb(i-3M8zVoRMt)3(uX6L45v-H#i=7oN<-iiS!(^mbLVtW zwU!tEVntJbLuMY$mPN+WS22p;7j)bt*wFl;?`rsyu>_4d$Igq@%@+0=A4G~%7nM=drETu+9D1ytV?Ls0Ti~G=oPeAkPwz@^ zYL?+1<-k15@J=7t&?4$WH&5D~f5(5~TGf8dXT}}VL&hjKzJ_A_IW3Zj(RlGWL-R8h zB(k(9)BXXsUfW%!P4W9zIfGuF#mmvl1sA~6WQr`D@De6&e%;PR^Jo$>O1;85(xpH<}xh3~Fv27jOO zliA^ncrsf40i>z+JSJ`w2$a+8$+HeWm`q6~O@NFajX7C{8VZsNpjqYE^?Hj+CDFnz zE*;5)3bVtt;87OYDD?$c8^m=j_c$0C(noN5$9g$>r4~f}-)OGVZ)|{yj&3{t2Zt{! zEVZtL^D!VwIl78 zr+r)4_&k%pvRih(}{73&G^#N)=$WhT?qVYysp`Zog}9{{dj5q8BOkdx3QwF)^W{M}*K)V{QRllbq$N^GkD=-25Rp@99X%}gJhP5VOV^agu7=Y&06#R^iv`aYudRsyB%{!UD=f-oH?lIi`K3Wj;txoCF6mC)I zU-Zl^7Y6b!ympd^_T_X}Ygk@Wh?`QPll1lE%+DFEkFK)xM%faw*&;egWOrAVAwphW zR+*`KCdc|4K3K$rAI!z;)oPRJnSr*~fgYsMLJ-c{s{9oTwZ4^?E`MrY5d;q<``X7)rxBTlg# z;4n*{IT!M0GJg8!e`9f#^4V*sGpdDbp@sqV+aWNDU~}^<1wQA^ zZnjQLhdA(BaeCRC7Ngh<^5B+gXTo}!iFK4OU4G}TxO0M9K5pQ(zE*7thCD1qIq3#>U)lI z!RAST(@XLygBMaW*_sML2q^m#(6g=Y5ESfVu6GmZL1cKMq{|`ebqG~pRHf1}U2y%P zPWK-C0mSIYbN;c_srS#C|AxX34v66&Ya9X=7cGr zX=Gx}y+v;F1dX2xVRT(m)=&=^PssH>F+>VX%2xgJS=+*!zq6!nBrrH|?g4>Y)~W@r zlFk+RmOdY7`R)ZBXJZ_b2c_@1g0Y6h-x(|zU<f&OfXoCQU&~Dax zDoU^?sUjL9d;&YMC;?$lU{o&xtWMr*^;U#nfF#9!toOLmy*0J?|GnXj)o>P6((ys%I03hrTD{) zd%^Y}4SOW$h9zMd=oEZ>1`lcjeAddmIkx93y`S^Wv;W##M&fkl41kV8?`j?hh&C=o z**|?4;SpKK^z^?n_Y)L;VkFSZ9}LEt>tP3j1KjNC0wkh4+Jnff20_t!j+l(CCT9!xWEHrhbenuK7jAaRz0?S;eg% zJq4n=3GkaKi>(Hvd^kqbrcCmz`>XUjJo6ItOUhZvvMZBP{|(}!g@fbA6_!+(yr>v| z=VKlgH^1>cwKzT{DyDy-Zx*3N1g>OAY4F>^7tbr&ZQmr^E>y{C7_r-X*@iS^2d5JW zEGIj}K@>E5?*^p_c{%1X%XAV8+2S~pJ}@Yv)!R@-;e)L zPyXfMKJl*jAyJwPb zSD`LOlcy=0`?G*iu^$-t;xAue>_f{y4)a93kM#FrE^vR-xfd)JUBtnvqe6h0?bD~@ zovt{Qt=@u7qRB8ggOot)LU8;0vy2`kwQ$k5DHsNFtdC?R6rANO2Tb^hBjSV@g6ivH zY4J#^Zo;%xeb-a~m`SpFi?ULeYknb3>^Fi;b7E8c4bw8yI&Gg)$$sbjY2Pg6_K|<| zkn8KMUrP!kdQq?X3R=ESiAEpWuFt*slyNKFMxSRy@Q1W~3tL+JqMlWq(6FW@-74JG zH9dJnD-5nR{=Iz^jd2}s#c00zr{JK77$JkNKOQZb5e$Gy+Zj$K~&XG^4r#hr)Xhs^-ezCRH(aR%mvU>hva7%IJ?qD@P&VuP@HDkIPF$r?jYSb|ICHuvYX-XSx;cKyqSw;sTuV|z zrI{xIkmXMC!&w?_n2clVkR(mkfOvg|!X=8?ckUOLG*&MEK_wjV?(5v1bI^4%ieM;V z_5t8$+3fnpg0CI#kax|$DZ>Q;O59|eK2dn$b)teC;44ZNF%wYT+=Z{~EueYJ>HA5TMe2aB<};5iEH_~#Xms_}@DBCA`BV+}15zKZ zXTPD0t^2nH3>eGf{*|+2i@NXGR*KJ0_dAQrB>d!F?v3$GRZg&V`B3-LyQ98%U7*za zpk%YPdW|h(QtvBTK&357W%OkHv1-4*k7tuX@_T4^Bz_@z7LTSQ{e_q;il37wem+(5 zy3p~4U)E^-LD+)0NTih!0i4PYp`CHMe0qAt^0?!}SySuKXh-0~81&?qf*)V`kf_x%42rT5fz z_y3~wz`0#I?xB%iq%X4#o+hk)<-Z8ai-eFZfc@z!mDnp7%Dz38FcLG1J=OZLN}-5K zT}--kh$l`{Ru*`RHgE-Flv;<6Nu-#|%DS)0-rZ5rGZ&GSbs{15{P*6aP4%_q+c;Y) z)m#S`AwBGB7=YYs#sImF(L_uS%Tk<3jNMxaZlhmx-5(fX`DbXp=?ZL1;kT{I{n{18 zk(37W(`fH`Fr(ia##vmE}|mmA0zZC~KZnVnkil$ZHSR^W0N#PM(R zhjH;2oJqZDVr3&c7kB4sx17Fi@i=^p$ zq`3e6Zi5Wv7<-+Pmt+*MbVXz%`hhW3mQ~3n>puW0$a0(htU0^dXJ^ZJI;>PIXhf4? z#`3dOxxZmjkjx?P?OjhBBS$87^RBU^754Y7{F)6Qm+|V&Joh%MB1v;g1DE|V$*y)NdH4iM zR-~}G!yJ8R3z);K_33Bcp=#Vv%#*f6$Xq&&GeN@_IsVF`Y^>1reZ7jB;6TUtSa1{S zFmv0{yyGt>;#&+yn0=62KvK*SO`I@ zY70&TWcYxvIN5rd7rH-%l0F&!=xfaE;=X>)1qX@|P&z?cmVW>~j zq85NrB~`7=%*Zv74Iezm=#FCx3kw|%h`$r$oQb^wjF zp=X9Z(5gkUURa_&vsb)IGiz%lc?E;gx-bAC%;i}kz78}=?*Pw9mVY#2cw=bkZYhWt zlT`jKd~o}#kEF!Rssl-~3G0_NwMnu)>tAF)z~)=FkuMW<5#5eaTe`=FFhn^;zlXP^ z>eQ0CY-(Km_egLE@AW4U93QD_@a|w6tSvFMxS7PM(sihGGp`DjUY_N~3@`e;tGK&( zn5edlWIFPu1P;P1tJ9X3Kl`8F79kt)Iqf zFh*@ghF4uXh5*Cpv3$}|g1*x^CudlqgHZqiySAfg_64T-_-UvIcC0y6mN-+SWMf(a zk7RD3qud4a>b)Y3{+TS}HoL1E5eo~33XiGUjud6Zg0^vr1Uu8T#q~1+B*vT7K?Z7? zM>h~C{Tc{c&kEsIN3X*6J&FGYOJ;80;Xl)4R_{EeW$bI59{X~pYOQh<7girn^hRZ= z0*b(EnkSdNK(`8@nZDwbrh7p#o*sAptbev+B=i%Q9-g^{w=aQa#wKeZTdzUOK7J)2 z|JJk3MsvYEJ0oMCYn6 zO%%1c;y8haW$pTjH^u)?hu$@&2pv{9heuj^F^x=Ax+hsNgS!dx)L5Uz_itM`qj7!U zDnZUrCb>I3>goBbXUYf1dhQbdtC)#@4RYgP1Z`$Y@jUkb2jQs%3UYfxp*oRMVrcQ_>ab8&*y!OW=eRAOF*R~tb>a7a{iO86QRw-ahc$n52q4HCf?v! z4+eUs$C#jtcC^}-%if?rv#|b4fS$01Fqa?3ee*Q-6tx4| zFOQDNojgc&6FxAKr>b}P=cUmxo?}*AjIuH)e_*h1YKiM=Qj_QM7)-vOu&MsBF-JK> zk;&v<>xi815JZU4odZjXtQl9Ix5FqMrbK+N(jrgAfFDO#4j6smz<>SsNGNNZcolh* zC}R`VF!$Ghun{eh8WO|c%ek4}ypw|`LN-A|CF^jTFkU76FEkbSfx0+_b?l53p$#-A!1@)o#k*()lUq*1FpSqg#IMFjyk1 z6Z?^!>JQN=Kw@rMwT-Rus}dn~anFQdT#~S|;wC}yG3@WoSkI zh#FN7NsdE$rjd8*HaSgzx0z)!>bMdjIi-e^Wv_xFyphh7C4hElqEE2wCFug#{@d>X z^*5(;|5pT*K|tw$LqG|Y!PoLA{sZ+iY5hM@Pw6EWNZuHUl2vKSaZrdP!|&bm)K{t{ zmPHDa4%ROxm;{yl&sJ1JqMpLAnd7|@H3q7a5`>(!VGwy9Utdms`C~e%CUo4v-wMR! zl^^MVR^P!LaVWQ!{oApL7=LoZHNF|3>T;&~-=bmZAHL;ciC!W9{9X*3OY~86*!(GiFmXKb;`A@@sg@ z?mbypC!?(P3d3Fon^I>Ety5oKfD#s@0y_x|bbsvW<~t zI-9d`TO7-y0CS+a4EyLPatdyVDHcT3VY4MLN1O6u1$b5Rbm60A;`;m16>?Y;-~1UA z8=Nk=dM*X^%K6Y%{0V&iTV(Agj&;GU{;9e&kvl$;{yjqg&)Rt%Kg~-A9t$N8*rtoh8MR56KytOF;2NYZo@y)MTc`&U5tYw9$#0c9gw5X8x*{i)TK_{esbc!>- z`UJKy+f8!JoKF>ufAwCJn<3~hengu-YLhi&S@nh@_M{QCc1}Ehcn;$A@k@atg@i-} zj5DFv0=g3f?0ICw3NIKx^Y~csOHV}-(mO7wMQy|UF3PM(v1U)Q+rQDx2R@S~A1feT z2n=z@(~%#NBH}UhZC%~)d7Lu*w(=Xhr`BFnqrEofwvF)_${d|>gD_58l&bn52Za#! zCy5%5a)H`X`h!8zx$N(^ba7f#(wE@9CwkD+axUUA9r0w@@f*cZTZBQ^9tGv>95ox2 zqZWe!O^S~yMuG87aRH-_14=Cxj-BfxU3(>(qO`i1r6~(d!2G<~wnJ`NZoPkDca9Pq zZ~+<(?t9h^9J!c`hLE2TZkLV+9|46&XD2`r2&cPOCn1<3D7Id}j8MbMwFA<`F`;q>*E34{l^D~Lm1xqJNJVZ_OeKVtAf z{%r3l=9%>v(v^DzdCsXNs0@Z6Odfrny6+HcH7lSNL^OissqlFZW;X_j3q;)196`1~ zqaYyS?{oU;)}{064PUwRKxcE%!-McK>=v@T)`1vCL_B6AC?Aqfw013f5w(w<&qo({ zw`&)WGx}v|XK8To+y#f3^bItxb1w+*5e|o3TEaoYfeMdG51}hSa@Ih06VAO135`$N&iB$KHEK|pX4^z zOhHBw>xg&H6*pVQ;&&0(xwn>UkSWk9LLAvjJQRYU_n>chg+S5d4v4$jB8{nz)YQk85v{TPA>v}hypeoU!KWDSW1LMt2H?2w?5$;C5*Ia8domZc&2E1iQ&iH#Z$%h^X09O+T%39-crqg|$T3 z9-OFeMaV}SOJ^n?))X#%$cU#+g_S;kTOgF?9fJR*qHq{%QgC%j+sUy2p{X!tzf2qs z*q1TNsgRSW^|sT62xh?6nm)C)N*rx+ipr8l&wT!N|FXqz>0$|T25&c?5_ujB_sFOY`i`6PEn4+|UUhzxwVLwX z`}LXbeI)nGV}~0e+Bn*W5c^HS*BB2Awrx!!{K;@!!~5LAnh@1Em9cNo$=RQS0OzI8 z^yleSrond>@~S0ntKUpwmRuL{JHjG)~Ha$MD*5^n2?Pz^l zbY3s){vPu2_e+#2Lm%T+bK}MrIh%7GMeX6S1jWx&gRLpc!;`xY@+UiL=BgB4_^}vb zJKTBxyg(*|WRXv>387cx{SR_+n&!%>1^&6$^x~2-js^|a#)`!)Hx!(%l=fQsPm#}? zq|Jk9wDY2cZx7?niGKX#7E5JRjAnlNlyS%Qq(4J9XgummLjzBw%%Pn8k_8w1L(ol$ z7V=r__w*aZ*{T#SW$aVGNztWhuGs)B@#~tkPJ#>inhby&x*Gd2O<6xYg3a0@0MBk= z-K(i+K0_U!Hm(RUr%z8v!rToR)o#k|j5O;u_uk*$|Hfq|#sv>U@Nln{&)S8_GsZ7g zYLB>YvgbD%zM;a&m3MZwH8$y+Nv;c8?UI#}>q#&(A|q5C6x4(I#5w-{y zw-iEcgRj8uZU%#|EURnI+ow^V!)h=d3kbe2a$^QN^}IhEdE8`Nk1m4?wsX2D=cLou zYavaC2GwtK9y#gzkK=4ohOMXbejUYMJ==-O#!P><*A&lL->TkP{#h#d!i{8@oVSHIhSSPnj`@r0^T|Fg8(tq=XfoO-`I@W^Q!0Ja z-xE`GCVXTs_u&7YfW05SfAKT2W$jPb&Oo# zU*{}znDRgG^Mu0F&VMm&2@~`WNW5+~7Ww5e7+5=vAgD}& zHS492Hkx!J*$bPjJy~kt_i{4Kp6W5HWe>+TmK>3KG9B}|ZQ8LBM6>%kuF^we{j0Bm z)Nbr|8%|d2;x`!hszr`(eM0~Cb!7Mr#A@+L)EF2C%l$k0?l;19Y31E^uHkFTpG1<6 z(d&!-@us3(EIFZO6fzUJgHm|103hvJ@Q>dbX)=B?qHxGL`7@?1NPcbDIFAJa{9Qq2 zU%4~7^OR`D!_tNJSQCph6#d9`;Y>MPgyo6C3Z-D-A-tt!ajh))1@U zvYEp7q7~xqqFeJ!n!Fy`EnBbjZkPfVjFoq##>=(^S8XzX!^8#!KBJ1Q zB}j5IZ9Z|Vbf?zIgTHdHd5J&m>Hq#G5>h_T`FjJBG>sdY6mDh|8f=;M+4NH9FME7d zq6FYyd5H$Ukrw=aEE>Y3EOwE8FLF$enP@2R(Sy?aL9vqFI!ha9_QebBnB?L3g{z2< zH)}=-UMa9$a?d<$0~^hd7iOCMNC6>OZwxVE`lMCU;pu8M>;Vl7bP(qK>~!z&I-@d$ z2*CX~p1dr1tZzSdQrG)tbxyjU^x<0vu2W_7ShlU5iz5rl&0Y&-&*)5hZZ!%-ZeX0( zeIH9LH?^_r#oru)r(r-yhad!mGED#A!^|?vkqA+?*CW>nH|4G1Tm(qj)aa`@d+yKAyUUnLv$xpv`p1 z)WjjJ1&+b_Sl@>(v`*1<(y`8_AZvTatNK}f4B|x>un3O@N|U34 zxOy{7L84hnoZ;^g5hNn&`X0&@9x#LU5fiIk(v1{Lu#$Ep)P}8(csLk^r?|xY@i%7( zk)m?%DA)In2ScgGk3kjx8N!5}(OR4gOPO^v8uR`F>t5&X6_whhc6sl+QIS%b^F2&? zjmsk**f;gy?l2UV#!5u_t!h!i8|f(h&z@>$7p|4#Hq&7xLMCet7=*gx`6KylcfkdY zmkP%$SJQb|^6mk2c)Yc8pQQtCc)`Dw?SH-|nUjwZzdSL%pLV&a48gM?{GL+slh=yR zv!4C(tyfNMwS(W4%=(8`4Y+}PWBJH?VnQ2kH%vO=>oFeQ{TA%I^!?XiCNjp} zT8_d6Cq#OGBn17YKE^I_Nr3fv3^@bTUcIi*yY^o`A`%ZMG#d2OIbcBLl>Bjx0Z*WZ zfAAJKc(Z%;!MjqRwu5<(srC#ZP|E+uKj|h*x-7Pjk6uG=Kcj$9ZvHBH;v2rmq`Az3JW3WftRQMZWG8LxNBrxlR3Ft5 zsp@vsjivh-6PNsP(50qSAmO6NiW%xjc!{B#{ww2Ew~;S z27_)fnz#?)$1h#JlwVE^jgQxb4)Pja1}rYb>E66ec{@zZGNhIWG9cx3ytrVbO`!r` z7^1y9u$SzaHK1atXg)BwdI%26HcSrF%kCOUmBhyg@VLhQU0vctWuE>EbyY+!r<-F> zmq#>1UwjmGvO2M+@VOpzwoK4x@dXbH9~C1{lZFoQyYureBq|XP9yt=?7eB%{x2or> z$fioF$)SxPL*rT7K0AGEezS&I|07UH{e;PqKEl%0G6QH?0~d9bbpOsssM#rLl|08d zc#CB7hZJpNec=i@<3_zY;^Wze-5T**vf#!{EH_n}ZL?8-uvGJ~W7Uim>s-hE@`2C# zuj8+bqY~vS?1lt!-+yxy1wwdK_2MS3mHv?eKlTS51+p0zqBs2zsP{JGU~YL_htMJ4HaTugpe|-O=*#Imj!gcwt0=L&$3f0q$YppEOhb1 zMU{*7ygan6^{c5@FE2gq6pGJ08ASUqF$5W*k#6B>eV(uHhB3Vj8Se5a%PWg^=&HMM z{&TE@bP9hdAQ|n@-{0hgJpX*{&D}LfG6?kI4oy%Q8bGgd4!9}c_O%o-=j0O@*QhUE z0m8ZoqiHAKpNEHqO4V4RTj16%sZ<%3|JXBReDA>U=5iv8-Y!X>0`D>fBwKiy+3{X? zfu557V%>Iu-p5+QExI)s3baZb3y-$f6ZrljCc$A(Dc?`0iI$BgOJ$kE{}~34x74?N z|LM4;MU(roFZv6IP`lSZ*@FKEH9*S0jV#Pj0_+%ClYdfpU4J0UJBXCOT`fdGlX`xF zQ@ZKi-8{-F`@Hu-EyPKhbk&OsO2ur-;+P_aPOa`N$ftiB$JwK(DvZD+cXIf2=CSir z3zY_YF$s6#vGt6*n6M5JKT2d~N zTQo_A8E2HT)<=52tSF`nk_|h5dx7P20092~4L|MyXb8yvx@Twg>fXS&fd`Djeeh|W zvp^H2AR2U|u0Y_1;r)~uK)e(d-f?bS&$f2Jdbt2#YK8qk85RB#H`hLpT7P?Ir*D^O zC8#Z+58NL-5OE9wil~a)X}#qdGrhZ}6QJCjIDql|*d3#oqQ!2rd=J!<8PJ+R-~auf zh&d&DthWI*Cr#)1ekxzM2yu`!C1R&mL64LpuC>*8zg%CZiD1ZfD5QDFHR@dymLFu& z%sN2=E}(<{fo;Cpj%iR|s?phr#}J?h0LQK^?Juds{c_Cnk1J8o06n8mxF6o#P~|)U zifzvT0E3vmL=(l-Hgd=U&L~TAxC-(ByG8OcrWA6H_)2YbGB;O*TIW|*(fnL21Ev8N z)%TCA@DGzCZqLm4{70wKA)lE|3UY>BL~0fMDWT$b?YpRM#7Qaks%kjL+hlB;is(-M z^NJ_}9;U}Fwqy(zIY6xQ4>t7cpZ?H+U&WqOi~CV1_YGd6x9WDtJNu?{?hB4k?ddsq zI&-g(?%ot3m9xq^7CG|g+rulIT}(`D^jMp}M@Xkn`z^?#P(pOyr6*p@f@dFDGYpak zRp_p4pInU(r_Le4*4QPxeWDH;V<{pKeZ+F0LKkiMAA(!c$yT=DG%zWG2#n@N(O$WD zIJ`9AD37|Ik=~zxE{+1+`0qzRzmbIYW8wq+(giwE%A=2#yD!)RdCVyci_7bxG=Zc` z1Hhik2J54kC@hy;I8lTp>#Q{d*nqQj2RK3cWudgPOga}3%^dl0gNV_?!=XK`@7!hW z?j>wvz4W#Ttnb|u?O(;u&_K2)ZQv+$;(}%-f02+$8&m(~KK`xbfY_hnP(A;iq_V{a z^e*Pus8h3-ZX$)ckg2g^L0Q>c*PY@m))@ghH7M^kv*Q&p=r2F(SEsecd!*_ct^eVh zrcE}-yW}#CrHe5X#~Sb^ahU42*{`=xrthM1heDcgrp4Grsbw+_J6w?N8pI2XiFg`` z4hg2Kha1!iM`tpM4!+53kjESZosEo;+=nqvsLUZq42YylnW}?W5i|YI5fg$2n(>+e zOj>ZKM{b{HG2%ulDk{6`;$wQMo9xvq4UR8JlgW^@8*fThvXK zD`k0mIsxfw=&!sqdyqG?jzkI=N=$Q;qGF_+e!5-qa6_{;kOfjtMKz+CoJU9LSBFgG zw;gc04;$+ z9$2@;J1;iV00{ciip-qc0@2&%>M7)4^)D?$|5w}2&1e$iYT zWh|*L&Q<^Ks6Q?~66A}ouHeqJTLV$}LT{N=(I1vj)RLY>7#FYXKANOA8cAI3-6W#K6h{Ln@6Fc%vU&dN`-qvl=bZghE1#lvl z#|1)HPv%Oc;!Hl@D64(~m!qFy-5=0F#UKwBKSd!o*pp#$A%PF11!$lxRJ|(pb16-J zcuneWv$>oyKhw&*(IF|#Dc*1mh3i;N&}d~WE`F87xZqiubffrWvQWexqDDNFJ^4eG zu45v*BKZ%(y(0Wkj{d_2aQ@gnKnQA#td4`)nr9Eo2i@L2_w)Z*lW`4t<&0I(Op^E- z;z(??9el<-Ipx>(SPDfSI9K45|B52dpITAp&KYOKOBIm0%B%MJ-!lRxFZVj_ zPjG#wbzYaK)H6*xjp9bgHybWuhPuMP4#S_{xjBK0GWd#Qc!~1xi84nvFpd{U|M|^_ zD^5%82y;Xg1h5aTt#2BS{Zu`kBGMLs8?h%&jmM*YLcD3+iGk;8F1|4!6cVgP7-@vR z|A#$b^_K$#%rc!_!w{OeOg8XIgp5Bue2|lyTA9|$6~_8v+v6rfok@>FP(n%9=4Ui6 z6*h#qO7tRjdXAXJjufEbFNM8I?f6n7SYf<~u`hn}blbtN-4~2xeI;&nlXxZ+;dQubVdv*g;(N3xF6%JqPwocCt_1+h_~*_iq?+Cw3n)|h`zc3_pkK4StpIZxl`&AoLaW9drA)Q) z-8*Rd!Z7OEqL$+|7nnv+l#*%bgHUeQY6&XqzDg?t7mj#ZShvn@&t%B(^S*n8ZKSw$ zubC2(iDT})J3Qe|!5Ak&4Ja;Q}T*A$&%Au(8++ST~f7A+e)Cr}G3s4ot zM_~ zNiJAt8(9%C_CSDgh!O+OAej^G?|JQgkqGlWx#ieD{4PSh!hc$wN@)eFSXfdzXHm*u z6!!;Jh(3KgjGZ{hV`~%0{=NT_m|p%5TP2W>o_M%_ww6PAVZ$KNUY1AZcP*1$wXZl; zj|i0#RPu)iu_jt&zXZD^bVdU3S)zsSYbot_!qxnUzxwqSUt3%u zB;lt5<~+Nfweh3>4BJKFg19(0aU~j!;o#c>j|Qpn7dp#^L|HFgE!jLN?y+ZI8u{<` zT=8K+c6HF<3dN)6XYL(M!?^F_ku3|WRyS}XKJyr&U7NZnQ}UJy3Emy8*@p0CV|f6% znBGb43K#{yv>G&C!~el|(c7QoZ2d>%k*klwnhfVn-Zui83fx`{FPBSv2ZC11t#12F z#UX;vcl1r-z2=0UE79e-OJpb$yA!RKsE-Ai)&zjWxR<7j>9usnF7Mq#lycoynVuYA zQ@>&}`k9Y}F5m7Km&amjojcdNFA2+^&pCVg`*2UY8I+G9SS_#af9C%7k6R}uBMKSX zT@y5*)vLaxmToyQAPUDF#vytfWZ2H)NEE$?V^o#`n1L1+4wr<#%;1TB|9bP_The6Z zT!Tn<2pNy)^eD;|$PVaV3j1sV$*(!s+mYT?A(4vTxkSI(jc=gTZW-6$Bgs4o>Umq& z5)eOM(jpJ{;N)Jr#=YP{81w8udZ{pBzWk~LxFnNhO|cQPH;|!Y?1PPCjnUJJ_p_4D zOCteBw_Dw6I=eSXH}Y5X61UaClhmWG{5dLIU()x3tLp95gIR7Y1kW_wsB|X;OgsYwaE@! zV!Ce&?_wmB(%F*ILV>kSE{iG$wqjm>m5cqnb!m`RX?&QbsY$LXTWl?plbN~p^^;GR zjh73!vJ8xyV(ayFM)s-vzsa0!sixP@j;EGE+21pM@izC4?Ev89MQ@ki80PE_~eBvMW;&li*?BvmJhDyXc9Fc_tRQ3!v=*;?4t)GWjj{9%aF0J}b0XwJBKM-RM?Q%`A6IxBw)IyrtH#lshlGA1(> ztca!qr|mYHByIE8)9DUw9-nO&YGpz#Z3dMGOdi|N;eImVR|pQP{s0lv$$Y?|&=b_f z@zbvQj^j;d_1GCnIqhgrr#W=ZnCp zt?2c3ydN0Hl-Fv~6$vJ8q@82aR0bV36Bm&z4J&#d_Byy80Gu8^u+3}@uANgG z9C7W=1`%?XCsnzvUU4?ao`QXwq9i!VD4jMh)U-6(rAT11K6WQo1_p1jv9}yCWLJ-! z9G)#Co0_M&f0Z{QwEvxs~&@lObWjDs`nK3_nArFMq%PT6TW6qMI}%@P^?JyNH_%1r>&- zj&6*#ec;P32+gUcggKfaaJYL_7^JL27tzaz`92J9KTGr@V>e5aw!p8@xq*U7%TN@7Zri3erHm)ZanKbI+HU`SlX(G+_Vq*>;{#H{_j zmMJ_Ll}7(i+);LYI%u_F%neWlqZn_y%)xyYRMG@-e}lMcLiVr0OhUBX_C<6+<{=!o zVs(3*Ksf|H6aNZ+3ekyKzwL`z0Y=`jed=6U(pgdmPBxI5IJ>|+64#;^8jtK|cco@g z1Kr&k1BlVcjfPg7kKZ%}KJnW|*S(Kdb-I8pR@{nUssDGBN1Kb*Gs!;uhzM`R_wg(8 z>r#E(j?>q2I@;NwB5YGETd2y|aS8{)X&Cys_r`ZdlcbWiG_bMX^MEQrfkwiS<_D$H zb*8F(vrk|ih=XOKo0VP%>&~k$hQJ2x#UzY|1?a16@Ne6J+bwO_TOR~q(3P~E_X^*fZ~_r%uVVAmtIFd3 zIwL}f;Wcb3T9YI=ix2w|j71&Zn<9fo})Y)7sAnG4*E1m~6J2&ACGF>b*whl7r+*+>gQ5jAgmHG@INi`Li z=Q112Jfp1jU~^+~?WEyx&P_D=CE^g_K6*YeP@{{m$Bb{@64K-z(M(>@yeA9N(HKAqZLA>l>omXuC{u#r0yz+$f8IpMHq+*M)bJahbN6L@ zgE+rOZ|o2GU6qfcIKBVkXVc8Fal<){zZfAF%nvy0H4I_#pL>g*WL?O;1gF{k-g~6@9ieWOhbY=QI`xw;#{HLB@O|V{Yby>|xv;xFDv8E8eRgXldzZu?5}{ zA|QV3^JTLS+B?g@QvrxjwjAz8Tk`b8<|)iFCQ-dIfpW8+S%un4(bUIdh@>@OF#zk4 zVLO6;Q9FaMo10;p*13;b8Gk3-nJsEzJ8x_0E3T?iADF6T1|?(w$EO2AfIeQ=np^K; z6(h_=^EAFJd!O81v*!`ixDJdhyibeQj;Uc&<3O}B3Wk{YOIdv_z7qDpyu9Ld?&Nng zn1*k>VCg3&yC1k(9ht`#3_W@)%{gocbr0G+@t{%p;`jq(VL}Dt7Q?rIjji!u$ll07H;k|`{CgCQkgJ%@^t&g0_Qj)GCvCBRJ$oPc^arMBTmS$Jauu*mow{6H zB?`6bfR742nrJU)&c$Ro za-zaW7F3l*Hg|9)pvjJkuM8RqF{G+ZCT9^?0rm<<^rr(v+p&~6w$WkL3b1N6F`h#k zg#R--xU~k*nqLR4zHjVGma~YXkNe%n^GU5YS%FpWQ3BM-b14P+$|nO${%HjWsGM# ztf)MM5&bYoZV7vWEKt}`w~-`Q+x;IahC$|{;|~?GRZHL>1Jf8mVR=e?*@)<~+lA&Z zLG*VI&A$aDA2ZpT;Y!23q9Kx1#L2TS*Lkgqc)BPDIZR zG+n+|x()OIW7vZG%5GO@oQXWUaF=Fuc_dHi_R}+3!vL-&cS-!uAUO zla~?15f!JOJ#Wi9)=gj@?x=kp?F28^V4C%yj^&aIWAI3c(7BN?_t+|C!&k4#T*eWw zn4*hra#37H?>|Bdfg5Z012j7YA@^*bSP9a2%BCto0%(1gP1vU2mV4`;mKuL{HVP!6 z=u3MtOk-5cb^r32Jp?Hw$E#%VwvLY6_2X^a9qp5Q7_rF1)q3aajLPrD7o zjjB4?K|`{nOP~@SlhV*?^fRKFGV-iuAmKUexP?2K6v+>TnNcGQ||l+euc^tc2ExGCqN5; zy?W3PV{CGV2>8Mb9?`1LdBqsPrtU=e97Z`><5`DvoDQr#baV1`7RI zS2XAf-6NM+Ugv6Q{8ckzh6PX%I=l;?k}PGrb&plHOYNpl?us!-2@cKq5h79aU=MGq z6!UQjzmSP@G{CcG2h2x-YpS-hKN3en>6Mx=mBuhF1Rt9Y*~>IWku1CP^B|`w;w9Me zU}fj*u>;Z}TMXkr2YqMQJC$~Cr_Q|MM2|L|KKXufz45Ni?#e19zZAthK{C>urJ@Md zg$CA-{&8{lPZ5*cIHK287I1kk^-gtG_moLC;kZrB!`Np5*n_T?M3}X!L1NI}WGQV~ zJTu@yi&w8iSz$RAft;8~+0JPDSS|kYkFc4kD$Ls);^mm&^Q-Gb+>VJ)5$}QEER_SP8m4%LSH_s2*Xf?OQT)sL*I->Cuwg}}{%U5_Z7LNVC@mS~g}H9`ywk4Cb#95f zK2s64!rqQPEeSALgj{z?)gc&dR!{67IX4qTqXhK2mNb)%i$Hs`G=i%5OerXD5Y=2P zW=@bl%dDlA!-@W%700cY?&l7y*ivR$I zU<#u5_O@>>{M_s`+_>HEt`a<;N>Glu-2-4`38XW|>m+HJR8Kg3E>`o8K_Ey62|mo?q0E?5=6n=wK` zLsuYO8__@i5%etzKpZT+Q=z5^ftiipZSdN?!O0{UPK;D z-aW^GwldE5$4om!_wI+>L>de;4Y!?B5odA}ucnKWBCdVzI*c11MyY&7j_jC77_$)$ z&?4x{Z;Ix-I%4V;iNu`CGp(O*$xM|v=?538{5c;aF+LAE!{)C7_y;B?@|NyM=Hy|Z zj-E}3h#C5-?JZyg{m!{>*NIleDDiw)PhG?0&t3wxn$Lv$P|#mA{^?q^o6s>^zpQ$9 z14RnT#9$E=x4-XJv|6D$w%X-ehg!=?xz#M(6D$ssZ(DrseWa`lxo(SQ5@=VWV-Y#9x>p+qS-YEPs(OY3jm ze-|i}ZY+II@CGKa;0w|_N1L-*Uc_I}w(*vfeOopSTnfZ~I<>`dvN+v`e+oOh@4oih zF1OgDERGR@8WPEDR4B1OEJcquxj~sMDunC5_l;dv&LBu~~QQ2;c%q+&$%|RM3 zbGdgLLD3v#qQh|p4^;iPdcC?ntKHPr4CofpZgBPTcA!!*UehR7QHWCYX~F2_zK`Fm z@&Cb5QxD4hj9e}MJP`%Bn|2?95cw7|(jRY<()x2o4O;GeUqRK_&KPvRYSzPVPr=up zu{cS8!f{q$n0prXn2(~_&jDkXKa>Y7&l-VhKNy65#-B{tIx4c92%D4R9%VEdm{XKB z^MFl037}WQ{)f39A}s_>z)!YeVxIpe$6_9W+e}^xdINR$ zMQ)Vjo}Iow19M8WhN5HOisfJLP=h!KnJvW6l=xc>b4zVl_6~NR;!oO38uA|?fho*% z7kmo_MsL}#{2zgtTe9<*^Dre(FMv$?0rCDRxNGt_2o4E*?L|9rnR4pq9aXScPZ!Uf zteLkK7S&M{_WMabV{M%O+`P@4JN};{<^ICMo;cB@iFIU4S7oe)>T?Lr@ii&@cI5Z1GSAH z)kGj(oGz+k-!?ed$2f;0q3*LKL$e3^;XgFOE4>HL6L%uLe<7K4~P~{y?d2#nC3j|VZtpQ-rT3#*#5V$uD|nI8hu+l z_vFWY>u5fhsT4=1^WIf-KR%YZ5&x-&CBCK&X?W^d?SC@Av$ug;QZFlKNMseH)c!xg z=1K|L+o3<+qzagg-4^Q{QG`)^D9cp%7GUNsn*l}g4H}#o&%+B?cXc%4>gA+Clf#np z-@8Kah(|hI7pRhGu70yw~*>T-IdW_|%!nMh$?VuCu+S@PL6l$p1-94BZ=KS+-?nsbTU?~Pk zg~X#Kt9!+EKPmXA01FD`n^h;)ZM)7;A(ZaW*A)QGhCak~t?10ZBJ|8nrj1a()kU>4 zbsY;RdS84VISyR>kPbFS2*|jH>^JN ze*!{(s^6j^?cLdf(wBle9)_%$!+O*ZE0GoGdV=>j@zJ^tU69n*gA2=*c_n>IGg$8( zzy*Gzl%ZhLvGSD+5+9VS795xIHF|PY)eaVU$eE(*27N`dD{&aZdpKe^o*JE-Fv60W z_(L&~_vG45e!EZ8BAw>gUAb?apA-AA7{{)6Hbf2?E?gZynC89OeY-Hg6DF6F_q~nA zEsKRrU%(3AwLNlVwMbC0MBSRJPx<^kA1jost@$}?RP;j(iMu~rrt(`C4FSG|=p4EL zRW=yYJ{~l{OqF@k;*hjJ5xZ^x4*E|NwJntm3KI9j!0OSHp48k$+Vdg;{L@-`f+q5n z@BnWaOguM38cSj_-0l-V{0UV!+04$tH_-V2s99%)4(;&5;?_k=< zjtE7U_i!jcy21V4KY>qi@277&4^GAz*x#X=ES)eILamDrpviWc8#)BXZQ%5k;nN0G z$uAL9;y{t9ZVQB{wKE7No_-ojOYd{4X4`C+qo=o5F*x=K_~!5_C~6!01fuL(!)Q}d zG)Ct8)C5!t7=IBkjE*hmTq&&TFWmsm-^FYb9X))u{P?%y4|O7J(C=0smOLzEZmo3k zo>3rxHv19u%uGPaT2wX;ME(dLN?gKF1C&<}Dipny6_|U@)MS=f;#)}`?R!rOp0fPP zL@MkUxx3MHdk>}_ieb@y2cb{>Ztf6P$!ry9EU>Ka^;`-R2ZV>iufho%QfUt#YMJ$V zOO%NitAvkpBvqM}wYYIoFM1;oD=qKFP`;lH=N{iqR};x>IkrvSWld&Kqiz4uAf=SC z9w7UwfuQK^#UvP8gIqZ;pki}Lmi1p#MeB02q1q(@fn=pO%87u;o%nP~bN|-H8MwUv z+GfT zKK+?Nm&^Dgj8R+vL`NuYLfH8=!9uGdL$OpIAGra?ot^ZN>Hma|%V?i+1R8q?zT%93 zH-u~_GJ(H!*5dyt9-QJUAEt0=8HKfUcQo0#s~VM_G)`zY;L}>VP6W*m)X;2^Wa;yu z(3{FQ@g61MS*pTsBKh_UZC6TS!XRgfy=`|pb`O~#yEuUfOc-aIuRR+87D5TMabDj~WKVJ$l5z(b_jc230#qD^1? zVDj%fteJ!y-+`*`MVr3k^CydK&S_@1Fy{sAW8v}_K?~J1IP0H6s5NJm`KzEH6D^Or|}bKOox?`6lk#ZNW1?6a~(`(AF|a~pXuZ56(T>w$kpzQ$%-}v zI>EsV+Hvtorg0cd+M6c}Mp79zZj{65wpHBCJEZ5VIVgS7?+|r&>nAWKw3vQW0FI?l z0x{6z#r=(zRytc_?-cql`?_`ku~q}>g#De{?E0F*2xo&TPBDbU@qU2^rfI) zt*bmI)?#_SK>*E_dXCbqymyRL&Q$93nDq!G6ngRA^x(9ScHQL3ezS>EwD~%uZ<%WU zYk=Yx(@#64>&92JaiOi7DcIllK}??jG3MOlo;!5Qx&qXD{B90`Ny2lOnZ1_3L&BaS zlV}tgQ#<6N6(6@<%w~fUOz1HdO5kzp{hUbf^VcW=LU!BOdbzz;3eCOE3f9RwKax+l zKQw;e6k0x*Ga&DA1Jq?E?@l-4tjDnre5^gEH3W0eDXTClyO2U9;0gyRsDViZLoXK$ z)Oks^i9hwXTlL5w`EtTJhnoKU?gWt;&)FjsbN(e|Fno*3Wn3i2QUk5q?4O%VV2qy2 zioYD<`qlBJ8~r*hN0jr9>=X)_4+YQI-as&pAMcK1c{(SDhbz-mxp*%;LbVm4`&XR` zAK+cT&qsdNb(K6X;!myGXzCOnKtJp)wCKRdUJh(a5wAddusvk)t9ZVkg=pz1ndKMX ztx}1FMDHgwc;=2cs9vUCn%KRHhGfzML71EPsWcWL&#Zf5wxacAtJGJp*)0s8A{N8v z{8!7fp3~E5JP1rT58a|k&-hZyLIC+`eHg;-Qwi!OyV zY$`srW1g!UuWE7Mj>JVd+Z94l-bV)C!lY}x{I(c2Um3hMJT{5vqgBaGD2eA;Rg<|B&v{8*wb#gZ_w?e* z`mmhX_l=m?_gb7A(33bcs8HepNt)`*gD)?s>X)k)e|{Y2TuIxyT}yAgn#P}Jtf&op=Xp%WdSI4G`eBsFX;+*^?Y zsa;Z7b#y9B9wH1T%d4KWvS(RC6id3H(yjbL%lncxjXYQoVAkuo{lb28@ZG#$5TSd= zJ=KF@IF)q?exgtB4V|mYzR|UbN=CX$wNAMU!^+`)(@w&UVUZRsDgr~ zfvZ)2LJ*|Dpy5LHkRXzB1aWi)M?ph(PJ0cj6T7<)x}Opo1Q?%1$fl)SVwrR)F5b9d z{};$6=o#NW)5@Gv`8`*f<-Q$mp=SeCiV762k%*VV>FOrTgymSu5{oOD!x5kf5II@O zgOV8SA6Cw6M=nwb_P(QBZ!p8>R=j$?E4K37s3uBP@}a=>%gEQ(ZK_}M6Pa@mc#D|pNe=!0=*boK z>>1-zor@*0bL2iqG~qC7?OdR{kixDhf*aeUbzR1M_dWt`*s1?HB>}>loqr1wgx9L- z&q;AoCntcfWS=yh7UgiABQ&ee=h%m_zc7N!ZE0@R^K3(XrK%m;u;xJ9==;{lMKf5V zI|tQtM3+&A50NBnOX*TpG7)E77w_%uTMAOwMBuA!v8J_8exhsHFMt6b%nTXIgEV8w z!q$t#T-*PD|5n_J!z*yMH=@;vkOxfkiw4<4Kwx|O^-_Eu6M@)}r5@hP?~GK@UD6R* zRH8k2aY%GEig{L6SyATuwo!LzQ^JylrCLd!t~9Q^JAWGJ!zAW@TgWfMQP`~9$ec*a zZx=T&K!eF~ts9$G>U>uZRhyw0^dsedEO9J93%uabiFxaHda+eJWY{mzO0?SF4LU7~ zr3F$UOfzX&Ep4RezKkE6C9rCFceky`X(bnu1GXf{-5nik9#M4KsC$pO2o{0SZS2rJszHP z*09;`Ibi-fEv3c^1_4Z;s0Ll9eUSNT^Dm-hnr~0B9K>V%U5R3oMhjxLf8+fxyBy!>^)c!RnMn%4NWKBjHEm=&D6+X=x zo10jW7}V=#Oo`r{8zJ&w!R5aE2E}VB*2xQXgwtq7#$8XI(@tyXL?-nbwIL{s@RXum z!F}(vtO;KefsM>XcxHy@{%-yvxAl)_qB^l8gjYaHgDXZ*FlOYD?mB`c0pIvm^#m@2 zg{>+}vv*0LF##EO!RADRsblDt99w&dZG>JblSF#`W&#x*{h3RnS>cepevdJ# zn_7!9DEWviihWpyz(UD9ngY0rZCteSoWb)dL5S%(JBQs9=*~e;0V~iO7|pgQK0;_; zo%ixv2P@T#R6K{wu#L;7I{p7lRnBVld^-Rhi)pUCx}LUzfR};)BgbfIl-q7a5`M!or2l_ zTl@Go34}Sk0arP&3Ev-Pm*d?^+cYF}s1hO*iq@4WitHU<%sKW(#mE$U(ArX@omZYg zklkOoIF>AuiN^X9GJkC^f2ssLkGdU6#zQp(ds(x>etb4(k5g1%izJCg9i(F4;tVL~{#GuqAI$gArh5qZ z2@k0UQjM?sxRt|rfO$Q$l3mGHO#?@nRmH^?IaW(al0XH|0?uV(J*qRH@Rf{HBOP3@ zxn#4dnmJ=6;$4vBW!34yo1jaF)^IEn+f6gkVfk|xb-2}th&Tkj1IV!7t$cwKe-w?A zFB+Cdi|tFVrB$tIivvR9l+#a4&1Tt`Jn8MF;I%qtZqx~l+XE+Mfxv2rt}9u6`i)`j zBY3IWU*}tj;*L94*_SMj9s!^nWNy?7Ca=fBY`^7>9X*GXM5HM{kWt1QJ%07EOUwuFH{7;Rx zlc|s^p>=gcbDP!>0SdpKbQOUCOn9 zv%2k?-w6daKW5jSHR#=0WO_cIVa%H|HBIfQS^jFSzGur`4ZYtnx1m&rS%xW)lMcJw z^vPn6bG?#T?N&a$%iDAOZ*Gsn`;wYJb0k3DO3Z|bNUzN&91?uDPv(0KAAJ_EI^yXt zbw~4%3+AI8u%Wrq(h=eL*YOI+X2OyNeD*Lhj*-@+lgT8p;~X=0hra>>(XKRnP1~vt zIC>87M{ng5pmGBw3NjXA^gY)b&Ad+zf|o&IR#^&4gJ41i~ntFTbO&cj&va?j@=pH>3+IEoMS(nm{YG@tS zGw8ml4xdRG>_`#M5EvIL5bbfEQyh?Sv;AOhgap!mI3J^V`p8B9{#Cul@8ci(cIOs; zEp5iq6A%0}Hpe9**h;Gc7ey$AD3X*#>VI>t{p!gd-0?zWJ8S>W`PwvEZ7#bcA4mAW zeoyZDLr=xUL6$(z_o0ayBiGJ%K&OYdxN(C_NND6)kqxV`lXV*kw{?0fb80cRNt0dv ziv1}I4j;Vju>f`_yrQv@LZRA^&q&m132$afqC8h~`|Qe)jEGow2tqD@cq&2)Qz;We zo3*wyBOjv?P{^>&@FoE`Mw~5UJmf5X_QnT-%vwVOOiaf*IClMpW@(4Kew|?vfLtf@ zkhCP)oQ0Vz?@!DBMW%y%4Nh|EG(X!$In(k%OE7TC>Yt@;U!{J$I|?QY*OQjtfCLwS zQ|)|_!SF9fM^e^*vLL=Bj_13_hI32r;MogUqp*KJ7oj(NS&BiK5MXL%Hkgl~8>j!} zU4)PL{A6fhptt=B+CP#&f&y6oi&^c+BJbQ_0YLilg1+vQ89uH>46o}6?&_qz<#Mtg zyTsmPT`w=?PsxMl${2a2?FVHENKXKMh%Si2-v%Y}My{s=S3n_BRZ#sTj_-#HpI5U- z9>n!gbtB9aii(42R%dvHWyCeYX{K(s{dGCW{XM|s+0$7;s)5E40+`x7b9oBR5iMz& zorL2VPUwxO2RP8i7V$b?IZUO!g-KnJXZQ6@-+hT?J=v^s2hI4r%cp?zu=Df?kvfCh zm`x`4B;-#NT$39lMt8ytLU!QIMDG9Q#3&N( zYdTtBSlF}wgv3l&sX-aQ#Y$xBtMe5n6}a;;?5Y-;Uk;7n0_WT($*Ozq!bA;*233ATIwprzi)?+AAu|C+HR9)|}1&~EzayL@}U<&_It z=XSv4rdNJ(OYLV!_JM37ilao&*&LOWvolV*d}9B$Jv6s=WiB_q&fK)WO9L|@kb?%% zVZOrH`^A~I!8Cm!@U%SdJ4LOOJ0TDtrWxZ_=zN&r8X0q`gnK1Aftrrk*H`a5P&%Xu zKu@J4bJV-C1Kw~h+5^=oeEJtu}Uf? zD-@z{szy~Hkg?@T6Sgks5xcEqVZEVZjXSI|;&dNe$={hGhl$JVN^7;KTP<bK}H6cb~`fZ-~Eqj;^i8_1oyH{PJL$3Z_fIYzM)JOsYd77sB&4Xs{ zqGA=e(L!*v00001B8t@sCEx?YJ!%5UyDRtf>>#0RXK8jsQBN{smX#=NWTv-6^crCckrtPM=Xv?OS8AQB}4sxgBdZ6>;plP8F$4F`|`2Y?yjM^G+`n0?1wuZ8W zeHI|@B0f@(9Os2RXKIoj_Q+4}S~*GkttF-cKFXfsuPz1JYMb|b=@^p}CHeM6uf>5w2!0^|D4lC> zjU?JKFN5lHCuGeD7p`X3wH?ohAsX_&n@x^;_R>A(YOIg(CnN=RzmmEb0 zSiGy&ak+*O)cV$fBzt7rx&~@?IRSyfTPS4(6`qaR9fI!ti6osqSC7K#X*qlte~`7MGx#9;FT3> zy78hX7|C4;1?0pJ~j*ZnHh%3XoaomjA;d*PsN$dD0`zWoj!nqrL3@ zA9uvr`*FJpoC2ex*GAn~%lUbs^h={+eG3pU{&l%UbOjuFB9^F>F~f!5Eooo9IQFWS z)~ySxetHV6oe#-&du*_vo%Q}%aciNhrqXN5=f&}KD{Y8FU*Q};r z=A{(Y7M1xoNht^8=^kA-1-4t&2nH zPDpdRx(bpxX9LriroK}c|RywlIg=luE!xc7u6kn?y)4FT9p+Vbbw-KT|k`5V!BLACu zoZN9s5`)P!Sa~E8SqbA`{HSg2Q9oK3UG}p1e4@-Q5l=zq>U^cO#u&Xab7|xLGZI zP0t_p|2+qNBmT5@-W9LlvnD33KQS-{902fokc2CM-*2{qm=PtXcrk8xP=f7>{!7tX z^ee!1^>HM^X0b=X^SeGhT^M{;w8UA0EsLGtjyb6TFp3lyIo4@!VTjIpWIwOj7tvJK zu+x-T!*AuS%5=^D?1*Cx>ncV^jSU(*^0i`3m@}VvQiGJujz}?SKns4E8kWzsvlX5p z5#EvzGFk%p5DJF^%J(-x2y>C1!7u6A0u<+PHap4S`#p9?Hl{x!3aP+w zV1((u@%g1SlOxmm!GsVkwml=qI6UKlmun9huCvj)^cD9m7hjfYJK;M}W}Nu*mS6FlXsfJf#RHvmm~*7n~9hi;|K7A8PfAET8-S9`Acv z!x@)CDKM}9r32i@V{Jfa49tIvak$Lwh4;x0lHcv%Q-)mHMMqe<$*~8p3_BCE0HQ`T z^)lzg)pU-dGTz^<8|6ga{YV+AIgtyo85K#i_<)D$le_K(xXxvu2xVoS$}~+y+20c# zdi8)A*&!GLgoS{gRbGFw~G7qUkt#7{?2tGOLwK2CbA5vejl8sV3`*r5@2hMS5y>Ph+D zr>0X}8vzs|e?rcg*FFJrYL{QvobE@&(TSVM6zL9&1fxjNbmF1eV6x7a>)n7JB*9y6 zf4Rq#S<^%C7h;y|o4y|I$WSY%|iKIEZzehji%B z0)xJ676x=@E8xc24AO)yO}P0f%^L@yiXOC~Me|WoZ45c#Ic~6bMLm~+H;`3m>FNpK z!~-=V=m0cpaY?VvC%QGQuu-#?42l&0CH^$_fWc$=X*G6=APg_0d)WE>gAnhahI6g1 z?3yQ>J@adwaRu_QC&A>l5lXikotXjW5{T?$B6o7bjGTQ2b1xe4 zV!qQR;40OaMW{sjr3O-w2(4&(XsP@*DAHT4&C9>WOes@MZE%f#D`sE@>S-I$(dy7? zE7T?khH3svRR$=UuM13ZY^c3{nQ=+B(xFbq0LtBdP8v&a<$5jOLxj2;5ujI>jigbi zMKsTvgZ`-^@8a0Fr_;Q}rYi9)f7CrdEKiC^i%8lFo6%*IQBF6+aSvFx1<{up(P+VY zMDsi~pbBRmm~ZZZ~WMMO_m)B9A}K_Ryaj zW?0IYKk_MMW%5k^(ya*ks{d8`;dQrvVzvj*k?&KFloR!$D>|b7WLhWt7R`3d?^_P;(CI~T@wF8)VAN_ATK)#l~Ri@5CWI-QXiq(Soa3yZkA9rD1q7a zaY^<;HQqyc>6e&MQXwE*cCi*I)JxU9g2cY{u%V0=9M?s-wH=`xh}%|wew2d#U~$Og);-=%BW@l+{Okozw%y8PH4 zvXR7xN=WHD*a|`c;`nI}XSj|E&@ddYm^Ea-X1~eVpHU+<(6)b@u>VK zZclRJ?N^Y4qwhLaVT9mwy0pjX*inbM@`3pBNb|3HR4_)-iVm0i6cA!Q+V0}-PLm8- z&>xj@$<$esn%n*j5CBX*Uh>=fbF0Rlgo%jz#fC@&6$ylFGq+}!%)Cv&sQNAz`TF|4 zdAKD6&nsM_&a3KlGa}2D!{2DxK8ma(F}as9L*}uVYg@2f!@aXlt8Wr7Oj9NLviOv? zG)&k|4V!+_HK{D4y(w$&&bvs=9Cfp#cKr51AvEO=9eI9^qs=m){#`np2kX%5jk}xN zx;ZNCR2LcMMjySUt&TE@q1sVVNzaY+x?%!TrrkP?DDae!c+)-vBLyL8mUgk-S=BOh zzBGCUdo4C|4tx|~+T3q#t9~kbLxkVpX3Vt)b%wGeo?Df8#f1}Z&|>c3;nTRAB7#T4 zQ_yPzVr{ey;_oZvl0SR+dDa3e3h1OIUm!GPlc?J|f&H`}XNBj1ahHH7-KoGRR2nlNvX{YD1|?Fy@jv5@_mq5N&%*nJ2w zIqw~C<;15!(7Qdxz@){xP3^hV{=+=9=eji0ozvGowjhgYwT%7?opI2N-|+&x0V;V$ z8;%Vv&7j?ANTAz@BR}6X8y#HOytDLfzroptZNz_4Z3XK~=vZ`*KWrMVv$+&C-h2fQ z7br7Xfhr$w8sb*JIwf*U;+(mOS+Ms&JUH}>i_KryX$Dem>8grGxr8+ zB7^wrP;_{+?v)YIKl8Z+Crj-Y_mB?mYnQ=P{~OGTQNa=*RfY6+R+lhZ|^Vx>Ww0H8<3t6OVWnrgT=~VqK#-#juss%fXG| ztmcN*lw)U`p7H03vdv8>R)_Hg_#(Ur`U~Or;Xc~c{1%6=eOHfev<`4L;WnzCvnYUM znT?T|{c1-o*cb~F>@Nz)g6j-^)R$vh6Jrnm!<%@3#1HGKQN z6AkT_Qsb$iii*Hb=@0!q_XXSXf}R*qW7nd~B@OwzeRCbs6?ow4O52c| zAJ7F&aS5CdzvuC!mQN}bwx9fkx+PO)QNqT1N86hcZH;HGFlYYL>pOK%EQJJb=7HEZ zVKjZs>c@uQXa%Uj+vvA8riqUpoS2a26(zY=EILd$^{6DOG8E?A9iMB}s9zbgp~ zaL+-c6@~bFu0U0YHXN0p`Y^Wqu5cuIeGCwaWHkB?sTlKgqJBL;*~L2JFfGlP$T`-0 zn;t+s4f#1j%Ma3MB3-|`QT3bFToVs2gMH0vEk~ykia)WcTIlkXk-N-a=qhVN8V6Ig zQvvTd9!RPE&)deWM2pTP^x^uWm(qL;Lx&+zr$%Zgkp=>LJn!JO`d%QN`54Efe}4)T zz{H`2Z(5+fOXpOp-(dQo72Z^K6rpyNlGXlM!jMtQ04V!@@K2ZA*}x~-cWrnbgX@=S z%XXR%Mby&b|LashgZBg9iSvWB)TnShHE4aTltIVO={EA~e`7TUmmVWJdeHn}pL$vn z&&;upTjJe+Mfu6vm!e0Bt;hlQxI@;Vu>K!;C?M?n;dsEwp;(p+us86f#y*U3GqRS{ zhdvZ;V(B%3k`O;#g`|TBjA(#lXlU}hS`2PqJ z`6ujRxuFR;b_}0i`l~ce3<}vE458;KNtYe5-|J_iMwJM?T}zcIM%cI<`}82!UgwRQ z3^hM@h|1N3*iYi{WHQ|i+~w{@O}edw!kwXy4HSQ1v5WE2oxh%DE!!+F+=L&@eg$^EH4tkSu6g@gxwR*|tESHr;)CzEq|qk0YpHT6_pSB=IZ$XyVfWG9#*ks44wGO+Ms} zfJX8SGQ*bjeg7MBNTdWZ)?v!1^<3``_UbitWJ__WMCTdhEYdOUM&S@21frD8-@_J- zy_|CySaNiGn-i& z#%)r|xLv3$*}TE4dP4Zbu`#-ArepDp^j}PzS6N!H>+e8|tm7Gj3w}|M9nev<8P&MN zU3r#pagrA`gliS|Z-Z=3!M?{Ap5o_%ztiw18=wnr<@U}C6~ukjIyet8eK{`a# zvEQ~$s-*9yCVx7W-)#u*$WN+cApFKeBG26zheYU^T60%y? z8YGyb&yz~!+k}Da(d)A6_^^H%QaGR-* zEOBUNO2WHe76Jh>^+c!5c%*S#Bc*4rIxjghS*q zA>~-u1)w-*FraMRiiKx^NqF3L1TCq3;q^U>x6?JK)gXoeI=Ak z(T~|){tUW9e*E=0X1(L?171XBb|s?u+<+SRNNjNxqS&44Y#Kw-hSc+U{mb&Ys6WMD zAk9*PEN0|mJ<083hFO!S&y(Qa08;1F;wM1r30-=;`2*5jltU68*r$Imy=&Tx_~5&@ zSBCUy;OjVJ#R3DwLMq?kuZso30eg6>dBL->PecG;^SAD$eyp?wft^hN6VcU;X{eap zG6m##K}TJ+`l;Sww6Dfq6B>`KHBj>mV?*IF*i=O2*dk-Yr-^sW9;TRv-{#s4BKV|n zpd?h@SZh`L)Wk+Q{5DV$D&!`H zxr6-KYDlf*dR4hdnIpZUOSnti3PK!JE8J9nT7rl0enbLPw|z>4n2UL1EQKLAcJl2x zA7M$IW_555?K+{r0000=)QY*jVB)kRvsPe)R_Y54fqp~=a@5J2S388fwrU$~m1K)n zH^A8YTyoYO^u7y;9zOuuQPUBY?gARB=baVsB9Re%CJEVua(Yz;_>ML_7wpo3IAQi$ zaLhZxgeY3vo5#FH9LzASV3{WEnbX+ShSJT~%;=t)(!`}E_R?oXxjFYMNRtIM@bLJ|(PfVyht_cw|n3D8)(e zazFqOD77H+mrk%D%AW&%)jmX*q==$RJ&MNinwsJ02Sb&IfA~Vz^%h8afHohK6f|CwemjnT`mmdL_*wm-fD}pE)|{Edg0O56F!bq z&EODGaFVKYQR;iA8!68}ti`EUR<-U+#~Qj3t7MMHZMv77`fO$94rTx%f&F^4u-=cG zm{-wjE6SYjW+GGi4hk=R%vp>T&^n+gcL0bvfE*x!62TB$f+d0^5F}6z*y=zEpg|JB z5L|*Kf+aY7;8b@~?fluf6aD-ijikC{EkyK?D-R_A($u?#!iwli|4@&l6cH%wpAqkS zth3--Hk9%AHJKrwq2BN)w46?K*$9y@D&%*+qT^P)D|J*~+*b@Nh(MerGs+^?6r1&2 zgdRkvUJqHKf9y;BmljR^rjLr5z2H!Bui_#BWHYze4G%?5an3CNIDDOqtEmZJwZ^2Y z+a0Z|^=87q8kG23ln!I9ho%HtlQvK5pdEXcQY+bWcdqCOqg-W`^(^OJ0OYj;8VgX_ z@fctpQ@Uisv{Q)y$~ag*G5P7wx^UA`>(}gR7uZks9|MsDsT;iL39Od3tvlWO+d#~`kdrMeh=uv-#YnpwXxfIPst8bT6>Isf+}J#C(>|45 z%1?4L1)pZA64t%6{(%5`IX(>q>2i~H%S`LF8IE(Vrl(yz68? z?#DNWR5EBXKKaFE*KH!+u#?Wo3$|1Wg7pqaljHHP)g9ZTrs<$*f_xm$pos9Jf<4YZ zGo|y5?no-6$RbN@CQqm9jU?807}2icIi59HbVqF3w~h~G>mQptyMVF?4P9bgcSb2h z>jI58?UNQMjZx9AIPwP!@E%lyC)6zv)ckn;-C^c^2j3=m$~)i>vxo1BH?yd%#J;+H zk~QEzSm74e<4UmSG{V~}Uwi+x{xAu=P7o2W8=#>8cUuT*tY=LZ7syE&Zq(l=FZtju zLg>&Q3z)}%R8}l4L>s__^=;(+zL z2AH@DQPNH&WY3lTbR7T~!}pG`F<=37BqcXmW>>IRPCfvt?n^$et2@z@Ea^W01INLU ze5?L431?$bU2PQRJ(t+Ij{hB)$+{t9BAjRWOy&Q`mc0MXV$>jdzHEbEC(aX$|8-yD zQ$U$}@%6HHS^2dvW7w=bKo@S}sLeM%*d6N{{e40bZodR$hrMH8!!~?S7_=l5cn_%0 X&&mBEQ|@l8;VtcDAz})3Abhsc{M literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_servers_calendar_detail_cancel_remind.png b/app/src/main/res/drawable-xxxhdpi/ic_servers_calendar_detail_cancel_remind.png new file mode 100644 index 0000000000000000000000000000000000000000..342f85154d053ca7c5d0709b69822db3788f59ef GIT binary patch literal 1050 zcmV+#1m*jQP)Px&)Ja4^RCr$Po6%9+Fc3hs_V7e*e3_d&$=1p`-36qtn>pF5ecD~ERx*fp-r$|A;7qKC%1%OiOw-91`LD?56<%jnI08NTYDWgwFbb+L} zQYryR7GNOord^<}>rX<6JplM9g!p0!c0UmDmk?r7Rn;U$KwN1IXqskwI-Q<$OWtDC z@6`_go(V*jWtT-!(C?MBl>q1z|Ci{D9XTaRs{#=mfKuv22tf%AYsQBlbrx}1mgU`0 zU(WwQ0hls8&R|^n0>E|1v^fX>GWoV`zY8HK(8Xwvd7d8>Me#CLSP%e8sfHrF!(b+e z_!AK)TU%StGX)6X>+35$PpiadN01XK<$;6D{$M-y1ukaK4Ps5x?6hr5apY^?_(HhD zC=1Y9A0gtcZIBeW+O|F1-`{`q0ZkyL)9F1T?%1A3#IvfZu6^E5&v*lH7~Ho!&riqt zX%}AzvMqUf$@6^ISB^ITt@SM;9@#QFDdnN>teBotO40IXbcr#p;02)b(I?YD$?UT% zD|~0f2CYl38k;-hdH%sirWb&|+3(D8C8_Jf!^2*dWwhcfHQ$PoFMuc0Omwxq9ZuV z0Vt&?cE&fMYW>rEb&cy-Re9<@-8#@sC0N99%Zm7y~0>CxFP~^FC4f#M2OH3BPorz=t8*;1_ zY9f*gBo|;32xWRqu}j@UjU%}LXC}J(oGAqru}^kw7Qju(Sr*V!1%8xL795yk`)aL? zMH%j3S5>YYY#r8 zoa_HKHxNS5Spik12aB^#00zzgT*n)=pKF2E6@bzAqiVz#=2RU@09?Q-rReaHZhrhX zEvO4P9E5HZm~JVg3lw33AZa&uW=|Iwg5LiLNRMue(Cv|97v)K=Qw#_=z>RxV0NZw%-)|`hBH~T#ZwI3Th%u`i83_P~)?4iR2NNtk UU#$AE7XSbN07*qoM6N<$f+L>K!TPx&?MXyIRCr$Pn^964K@f)jR#Fyu(i>o9smddsAaVlZ2_lc+gL(q>1nL7|6s-g`9n)?KQaLG@&K>N z$9TbEAfM3kYjiXZUFPBeUW3nok?~`z8K?d}oumz6J^?IWNELVFcB{Z*yjtYFLpKw$V2j;H2&ODl-bniUA=0E(A)LBZ!!8O z4u?blign&&!j(?RQR3R|?o-ERq(&g_7$`)1f8^Kb+@}B6ow(%_ryK!4p7S_N8mEOb zUeB!ski!>GK!1?OrmY-n0L3O>F=J1|@5Zmut|i;h&x#FhMTCTPC|Chdqd#CgglVeW z{yC2=+Y!-0Q=af)4fncm804{GBhv~%)ad63o3mQF7VBJMf|_xrAIpl89e@TugpGvC z_T4H~T)O^bUlE~TRZ$WH2zGUhyD%eXIr?LHQQ-$dS-Ojq1E{lF=c|p*`^VA3#Ke2N zCf{imOX4X}4&ainHT!w7a1&$0+IS6C<#bh9o*aZ*ze>94GE|%#fNt4q;eNINir=}! zgXv_bW}t43HYNSa0fd*dVLJ^;lpVJnEsh>Tz7;1zIe-PJ=(?&bX8@!w=!(2l?qqvc zdSo6T9SK+DrE)V5m>}S4AhHmUx}q!cQn^_OWFhdLfGbZF<$#DKDAK;%paURXl8XRD zRDnBwjkc1;wdDJvrYIw=3CIBGg^IVK5@W;K>MBFhnxrv+;946a^cEKpD&4BvL(4co zQI#omh-!M{%m_eS61=4=1}002ovPDHLkV1jA-^CtiR literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable-xxxhdpi/ic_servers_subscribed_game_management.png b/app/src/main/res/drawable-xxxhdpi/ic_servers_subscribed_game_management.png new file mode 100644 index 0000000000000000000000000000000000000000..2cb88a84a934c9b8f7531a911dd0706c760a0fa2 GIT binary patch literal 652 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)jKx9jP7LeL$-HD>U~=9)a>0*V zrxfi?X^cIPD6#Kq)P1?CjU7KM`yS6u&Yx2JZ%WXvs7(`uZJs4-K6TEIjXf}@cy8sy zSk0$Dc^=H4a_R81Z`Vq+{?E7F{=Ykh$)Y>J%kr0$WjbSf;*Y@W+S(NEdimoYAAHL@ z{(R1Du^pCEy=>ED1q-eFlD~htoSU$_)8}$YeS)^$uFf^rU!NEJ_^xN$C%vU5yPy2J zZZ|jpt{I%fF9f){kSD zx1fQ^iBV#EBVjWBio^3m54c00ANrtO`}~jq&{#>5)1$U literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/bg_datetime_picker_mask_bottom.xml b/app/src/main/res/drawable/bg_datetime_picker_mask_bottom.xml new file mode 100644 index 0000000000..afdeab0233 --- /dev/null +++ b/app/src/main/res/drawable/bg_datetime_picker_mask_bottom.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_datetime_picker_mask_top.xml b/app/src/main/res/drawable/bg_datetime_picker_mask_top.xml new file mode 100644 index 0000000000..6e93e72f92 --- /dev/null +++ b/app/src/main/res/drawable/bg_datetime_picker_mask_top.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_server_detail_remind_time.xml b/app/src/main/res/drawable/bg_server_detail_remind_time.xml new file mode 100644 index 0000000000..96a761dbdf --- /dev/null +++ b/app/src/main/res/drawable/bg_server_detail_remind_time.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_server_detail_remind_time_disabled.xml b/app/src/main/res/drawable/bg_server_detail_remind_time_disabled.xml new file mode 100644 index 0000000000..76dcdba322 --- /dev/null +++ b/app/src/main/res/drawable/bg_server_detail_remind_time_disabled.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_servers_detail_cancel_no_data.xml b/app/src/main/res/drawable/bg_servers_detail_cancel_no_data.xml new file mode 100644 index 0000000000..96fd57e0ad --- /dev/null +++ b/app/src/main/res/drawable/bg_servers_detail_cancel_no_data.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_servers_detail_cancel_remind.xml b/app/src/main/res/drawable/bg_servers_detail_cancel_remind.xml new file mode 100644 index 0000000000..e53aeb6c44 --- /dev/null +++ b/app/src/main/res/drawable/bg_servers_detail_cancel_remind.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shape_f5_radius_2.xml b/app/src/main/res/drawable/bg_shape_f5_radius_2.xml new file mode 100644 index 0000000000..b4295e2cf0 --- /dev/null +++ b/app/src/main/res/drawable/bg_shape_f5_radius_2.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_shape_white_radius_12_top_only.xml b/app/src/main/res/drawable/bg_shape_white_radius_12_top_only.xml new file mode 100644 index 0000000000..0e8bdb2eb4 --- /dev/null +++ b/app/src/main/res/drawable/bg_shape_white_radius_12_top_only.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_gamedetail_more.xml b/app/src/main/res/drawable/ic_menu_more.xml similarity index 100% rename from app/src/main/res/drawable/ic_menu_gamedetail_more.xml rename to app/src/main/res/drawable/ic_menu_more.xml diff --git a/app/src/main/res/drawable/ic_menu_gamedetail_more_light.xml b/app/src/main/res/drawable/ic_menu_more_light.xml similarity index 100% rename from app/src/main/res/drawable/ic_menu_gamedetail_more_light.xml rename to app/src/main/res/drawable/ic_menu_more_light.xml diff --git a/app/src/main/res/drawable/selector_servers_calendar_detail_item_remind.xml b/app/src/main/res/drawable/selector_servers_calendar_detail_item_remind.xml new file mode 100644 index 0000000000..e7264fe9b4 --- /dev/null +++ b/app/src/main/res/drawable/selector_servers_calendar_detail_item_remind.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_servers_calendar.xml b/app/src/main/res/layout/activity_servers_calendar.xml index d0c861a85f..f82624338d 100644 --- a/app/src/main/res/layout/activity_servers_calendar.xml +++ b/app/src/main/res/layout/activity_servers_calendar.xml @@ -148,6 +148,46 @@ app:layout_constraintTop_toBottomOf="@id/recycler_view" tools:text="官方服:腾讯版 \n混服:九游" /> + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_servers_calendar_remind_time_setting.xml b/app/src/main/res/layout/dialog_servers_calendar_remind_time_setting.xml new file mode 100644 index 0000000000..7032477ccb --- /dev/null +++ b/app/src/main/res/layout/dialog_servers_calendar_remind_time_setting.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_servers_calendar_time_setting.xml b/app/src/main/res/layout/dialog_servers_calendar_time_setting.xml new file mode 100644 index 0000000000..8086fbb8a0 --- /dev/null +++ b/app/src/main/res/layout/dialog_servers_calendar_time_setting.xml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_servers_calendear_detail.xml b/app/src/main/res/layout/dialog_servers_calendear_detail.xml index 07eb1bb979..18c6c107b4 100644 --- a/app/src/main/res/layout/dialog_servers_calendear_detail.xml +++ b/app/src/main/res/layout/dialog_servers_calendear_detail.xml @@ -6,6 +6,7 @@ android:layout_height="match_parent"> + app:layout_constraintTop_toTopOf="@id/calendar_hint" + app:layout_constraintBottom_toBottomOf="@id/calendar_hint" /> + tools:text="07-02详细开服" /> - - - + app:layout_constraintTop_toBottomOf="@id/close" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_servers_calendear_detail_item.xml b/app/src/main/res/layout/dialog_servers_calendear_detail_item.xml index 137c8fbe31..26d68b8b09 100644 --- a/app/src/main/res/layout/dialog_servers_calendear_detail_item.xml +++ b/app/src/main/res/layout/dialog_servers_calendear_detail_item.xml @@ -4,8 +4,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:paddingTop="10dp" - android:paddingBottom="10dp" + android:gravity="center_vertical" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/feedback"> @@ -16,6 +15,8 @@ android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginStart="16dp" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" android:text="时间" android:textColor="@color/text_subtitleDesc" android:textSize="12sp" /> @@ -25,6 +26,9 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" + android:paddingEnd="8dp" android:text="区服名称" android:textColor="@color/text_subtitleDesc" android:textSize="12sp" /> @@ -34,11 +38,35 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" + android:layout_marginTop="12dp" + android:layout_marginBottom="12dp" android:text="备注" - android:layout_marginRight="16dp" android:textColor="@color/text_subtitleDesc" android:textSize="12sp" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_game_server_test_v2_list.xml b/app/src/main/res/layout/fragment_game_server_test_v2_list.xml index 49cb38fe9e..d55ac9c017 100644 --- a/app/src/main/res/layout/fragment_game_server_test_v2_list.xml +++ b/app/src/main/res/layout/fragment_game_server_test_v2_list.xml @@ -23,12 +23,15 @@ android:id="@+id/list_rv" android:layout_width="match_parent" android:layout_height="match_parent" - android:visibility="gone" /> + android:visibility="visible" /> + android:layout_marginTop="-50dp" + android:visibility="visible" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_server_calendar_server.xml b/app/src/main/res/layout/item_server_calendar_server.xml new file mode 100644 index 0000000000..7a142de3f5 --- /dev/null +++ b/app/src/main/res/layout/item_server_calendar_server.xml @@ -0,0 +1,28 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_servers_calendar.xml b/app/src/main/res/layout/item_servers_calendar.xml new file mode 100644 index 0000000000..e591991a3c --- /dev/null +++ b/app/src/main/res/layout/item_servers_calendar.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_servers_calendar_remind.xml b/app/src/main/res/layout/item_servers_calendar_remind.xml new file mode 100644 index 0000000000..7055b3af77 --- /dev/null +++ b/app/src/main/res/layout/item_servers_calendar_remind.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_servers_calendar_remind_time_setting.xml b/app/src/main/res/layout/item_servers_calendar_remind_time_setting.xml new file mode 100644 index 0000000000..94f6a12154 --- /dev/null +++ b/app/src/main/res/layout/item_servers_calendar_remind_time_setting.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_servers_subscribed_game.xml b/app/src/main/res/layout/item_servers_subscribed_game.xml new file mode 100644 index 0000000000..bae292b2fc --- /dev/null +++ b/app/src/main/res/layout/item_servers_subscribed_game.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_servers_calendar_detail_list.xml b/app/src/main/res/layout/layout_servers_calendar_detail_list.xml new file mode 100644 index 0000000000..95396dedcc --- /dev/null +++ b/app/src/main/res/layout/layout_servers_calendar_detail_list.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_servers_calendar_detail_no_data.xml b/app/src/main/res/layout/layout_servers_calendar_detail_no_data.xml new file mode 100644 index 0000000000..2322c311d2 --- /dev/null +++ b/app/src/main/res/layout/layout_servers_calendar_detail_no_data.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_article_detail.xml b/app/src/main/res/menu/menu_article_detail.xml index 0d62f21478..bdf8b539e9 100644 --- a/app/src/main/res/menu/menu_article_detail.xml +++ b/app/src/main/res/menu/menu_article_detail.xml @@ -10,7 +10,7 @@ diff --git a/app/src/main/res/menu/menu_comment_detail.xml b/app/src/main/res/menu/menu_comment_detail.xml index 3b63d7452f..baa35c84ab 100644 --- a/app/src/main/res/menu/menu_comment_detail.xml +++ b/app/src/main/res/menu/menu_comment_detail.xml @@ -4,7 +4,7 @@ diff --git a/app/src/main/res/menu/menu_forum_video_detail.xml b/app/src/main/res/menu/menu_forum_video_detail.xml index 282fb29378..f85c1266ce 100644 --- a/app/src/main/res/menu/menu_forum_video_detail.xml +++ b/app/src/main/res/menu/menu_forum_video_detail.xml @@ -3,7 +3,7 @@ diff --git a/app/src/main/res/menu/menu_game_detail.xml b/app/src/main/res/menu/menu_game_detail.xml index ff48abdbab..0caadc7a09 100644 --- a/app/src/main/res/menu/menu_game_detail.xml +++ b/app/src/main/res/menu/menu_game_detail.xml @@ -16,7 +16,7 @@ diff --git a/app/src/main/res/menu/menu_server_calendar_more.xml b/app/src/main/res/menu/menu_server_calendar_more.xml new file mode 100644 index 0000000000..32aeeda7b9 --- /dev/null +++ b/app/src/main/res/menu/menu_server_calendar_more.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8dfe8c52ec..a095ca582b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -471,4 +471,50 @@ QQ小游戏 存档数量已达上限 请选择替换覆盖的存档 + 加入订阅 + 订阅开服表,订阅后助你获得一手新服信息 + 通知时间:8:00、12:00、18:00 + 操作 + 取消订阅 + 不再接收开服消息 + 取消 + 游戏发布新服信息时,您将在消息中心收到通知。为了避免错过通知,建议您开启微信公众号提醒 + 游戏发布新服信息时,您将在消息中心和微信公众号收到通知,不会错过任何开服的消息 + 游戏订阅成功 + 我知道了 + 设置提醒时间 + 订阅游戏管理 + 开服表 + 开服提醒 + 开服订阅 + 游戏订阅 + 设置时间 + 取消 + 确认 + 当前日期暂无开服信息 + 游戏发布当天新服时提醒 + APP发送提醒信息 + 微信发送提醒信息 + 添加提醒 + 取消提醒 + 提醒详情 + 区服名称 + +%1$d + 今天 + 明天 + 待提醒 + 即将开服 + 已开服 + 有新服 + 已过期 + %1$s %2$s + 已订阅 + 取消订阅 + 确定取消订阅吗 + 确定 + 取消 + 您添加的提醒时间已过期! + 至少勾选一种提醒方式 + 区服已开服,不能设置提醒 + 网络异常,请检查手机网络状态 diff --git a/module_common/proguard-rules.pro b/module_common/proguard-rules.pro index 5a6d71fa65..7b37d9ccaf 100644 --- a/module_common/proguard-rules.pro +++ b/module_common/proguard-rules.pro @@ -74,4 +74,7 @@ ### keep models -keep class com.gh.gamecenter.common.entity.** {;} -keep class com.gh.gamecenter.common.eventbus.* {*;} --keep class com.gh.gamecenter.common.retrofit.* {*;} \ No newline at end of file +-keep class com.gh.gamecenter.common.retrofit.* {*;} + +### NonStickyMutableLiveData +-keep class androidx.arch.core.internal.** {*;} \ No newline at end of file diff --git a/module_common/src/main/java/com/gh/gamecenter/common/constant/Constants.java b/module_common/src/main/java/com/gh/gamecenter/common/constant/Constants.java index 8443077bc5..012c7d0c0d 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/constant/Constants.java +++ b/module_common/src/main/java/com/gh/gamecenter/common/constant/Constants.java @@ -493,4 +493,8 @@ public class Constants { public static final String SP_SHOW_COMMUNITY_HOME_VIDEO_GUIDE = "show_community_home_video_guide"; public static final String SP_COMMUNITY_HOME_VIDEO_LOTTIE_LAST_PLAY_TIME = "community_home_video_lottie_last_play_time"; + + public static final String SP_SERVERS_CALENDAR_BY_APP = "servers_calendar_by_app"; + + public static final String SP_SERVERS_CALENDAR_BY_WECHAT = "servers_calendar_by_wechat"; } diff --git a/module_common/src/main/java/com/gh/gamecenter/common/constant/EntranceConsts.java b/module_common/src/main/java/com/gh/gamecenter/common/constant/EntranceConsts.java index 7d4cded9f2..dea5cb5f23 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/constant/EntranceConsts.java +++ b/module_common/src/main/java/com/gh/gamecenter/common/constant/EntranceConsts.java @@ -228,6 +228,7 @@ public class EntranceConsts { public static final String KEY_ARTICLE_OPEN_IN_NEW_PAGE = "openArticleInNewPage"; public static final String KEY_ONLY_CREATE_DRAFT = "onlyCreateDraft"; public static final String KEY_KAIFU_SELECT_TIME = "kaifuSelectTime"; + public static final String KEY_KAIFU_TIME = "kaifuTime"; public static final String KEY_POSTER_PATH = "posterPath"; public static final String KEY_BLACK_THEME = "blackTheme"; public static final String KEY_FROM_LOGIN = "fromLogin"; @@ -306,4 +307,10 @@ public class EntranceConsts { public static final String KEY_SHOW_DOWNLOAD_MENU = "show_download_menu"; public static final String KEY_IS_FROM_MAIN_WRAPPER = "is_from_main_wrapper"; public static final String KEY_IS_FROM_HOME_TOOLBAR_WRAPPER = "is_from_home_toolbar_wrapper"; + public static final String KEY_SHOW_REMIND = "show_remind"; + public static final String KEY_CALENDAR_YEAR = "calendar_year"; + public static final String KEY_CALENDAR_MONTH = "calendar_month"; + public static final String KEY_CALENDAR_DAY = "calendar_day"; + + public static final String KEY_SERVER_CALENDAR_ID = "server_calendar_id"; } diff --git a/module_common/src/main/java/com/gh/gamecenter/common/livedata/NonStickyMutableLiveData.kt b/module_common/src/main/java/com/gh/gamecenter/common/livedata/NonStickyMutableLiveData.kt new file mode 100644 index 0000000000..02cb8e97be --- /dev/null +++ b/module_common/src/main/java/com/gh/gamecenter/common/livedata/NonStickyMutableLiveData.kt @@ -0,0 +1,54 @@ +package com.gh.gamecenter.common.livedata + +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import java.lang.reflect.Field +import java.lang.reflect.Method + +/** + * “非粘性”状态的MutableLiveData,支持用于事件传递,可避免数据倒灌等情况发生 + * 主要特点是observe方法被调用时,不会回调onChanged事件 + */ +class NonStickyMutableLiveData : MutableLiveData { + constructor(): super() + + constructor(value: T): super(value) + + override fun observe(owner: LifecycleOwner, observer: Observer) { + hook(observer) + super.observe(owner, observer) + } + + /** + * 通过反射保持Observer的mLastVersion值与LiveData的值相同, 实现LiveData的“非粘性”状态 + * @param observer observer实例 + */ + private fun hook(observer: Observer<*>) { + val classLiveData = LiveData::class.java + val fieldObservers: Field = classLiveData.getDeclaredField("mObservers") + fieldObservers.isAccessible = true + val objectObservers: Any = fieldObservers.get(this) ?: return + val classObservers: Class<*> = objectObservers.javaClass + val methodGet: Method = classObservers.getDeclaredMethod("get", Any::class.java) + methodGet.isAccessible = true + val objectWrapperEntry: Any = methodGet.invoke(objectObservers, observer) ?: return + var objectWrapper: Any? = null + if (objectWrapperEntry is Map.Entry<*, *>) { + objectWrapper = objectWrapperEntry.value + } + if (objectWrapper == null) { + throw NullPointerException("Wrapper can not be bull!") + } + val classObserverWrapper: Class<*> = objectWrapper.javaClass.superclass ?: return + val fieldLastVersion: Field = classObserverWrapper.getDeclaredField("mLastVersion") + fieldLastVersion.isAccessible = true + //get livedata's version + val fieldVersion: Field = classLiveData.getDeclaredField("mVersion") + fieldVersion.isAccessible = true + val objectVersion: Any = fieldVersion.get(this) ?: return + //set wrapper's version + fieldLastVersion.set(objectWrapper, objectVersion) + } +} \ No newline at end of file diff --git a/module_common/src/main/java/com/gh/gamecenter/common/utils/SensorsBridge.kt b/module_common/src/main/java/com/gh/gamecenter/common/utils/SensorsBridge.kt index 30ff1bf106..8e87084498 100644 --- a/module_common/src/main/java/com/gh/gamecenter/common/utils/SensorsBridge.kt +++ b/module_common/src/main/java/com/gh/gamecenter/common/utils/SensorsBridge.kt @@ -55,6 +55,9 @@ object SensorsBridge { private const val KEY_SEARCH_TYPE = "search_type" private const val KEY_SEARCH_RESULT = "search_result" private const val KEY_IS_NOT_PROMPT = "is_not_prompt" + private const val KEY_SESSION_MESSAGE_TYPE = "session_message_type" + private const val KEY_REMINDER_TYPE = "reminder_type" + private const val KEY_MESSAGE_TYPE = "message_type" private const val EVENT_GAME_DETAIL_PAGE_TAB_SELECT = "GameDetailPageTabSelect" private const val EVENT_GAME_DETAIL_PAGE_TAG_CLICK = "GameDetailPageGameTagClick" @@ -117,6 +120,15 @@ object SensorsBridge { private const val EVENT_SEARCH_RESULT_RETURN = "SearchResultReturn" private const val EVENT_SEARCH_RESULT_CLICK = "SearchResultClick" private const val EVENT_ARTICLE_SEARCH_TAB_CLICK = "ArticleSearchTabClick" + private const val EVENT_MESSAGE_RECEIVE = "MessageReceive" + private const val EVENT_MESSAGE_CENTER_CLICK = "MessageCenterClick" + private const val EVENT_MESSAGE_SESSION_CLICK = "MessageSessionClick" + private const val EVENT_MESSAGE_ITEM_CLICK = "MessageItemClick" + private const val EVENT_MESSAGE_ITEM_LINK_CLICK = "MessageItemLinkClick" + private const val EVENT_LAUNCH_SERVER_SUBSCRIBE_CLICK = "LaunchServerSubscribeClick" + private const val EVENT_LAUNCH_SERVER_SUBSCRIBE_CANCEL_CLICK = "LaunchServerSubscribeCancelClick" + private const val EVENT_LAUNCH_SERVER_REMINDER_CLICK = "LaunchServerReminderClick" + private const val EVENT_LAUNCH_SERVER_REMINDER_CANCEL_CLICK ="LaunchServerReminderCancelClick" private var mIsSensorsEnabled = false @@ -1949,4 +1961,202 @@ object SensorsBridge { trackEvent(EVENT_INSTALL_GAME_FINISH, json) } + /** + * 事件ID:MessageReceive + * 事件名称:消息接收事件 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param sessionMessageType 会话类型: 游戏消息、系统消息、客服消息、互动消息、运营消息 + * @param messageType 消息类型 + * @see EVENT_MESSAGE_RECEIVE + */ + @JvmStatic + fun trackMessageReceive( + gameId: String, + gameName: String, + sessionMessageType: String, + messageType: String + ) { + val json = json { + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + KEY_SESSION_MESSAGE_TYPE to sessionMessageType + KEY_MESSAGE_TYPE to messageType + } + + trackEvent(EVENT_MESSAGE_RECEIVE, json) + } + + /** + * 事件ID:MessageCenterClick + * 事件名称:消息中心点击事件 + * @see EVENT_MESSAGE_CENTER_CLICK + */ + @JvmStatic + fun trackMessageCenterClick() { + val json = json {} + + trackEvent(EVENT_MESSAGE_CENTER_CLICK, json) + } + + /** + * 事件ID:MessageSessionClick + * 事件名称:消息会话点击事件 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param sessionMessageType 会话类型: 游戏消息、系统消息、客服消息、互动消息、运营消息 + * @see EVENT_MESSAGE_SESSION_CLICK + */ + @JvmStatic + fun trackMessageSessionClick( + gameId: String, + gameName: String, + sessionMessageType: String + ) { + val json = json { + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + KEY_SESSION_MESSAGE_TYPE to sessionMessageType + } + + trackEvent(EVENT_MESSAGE_SESSION_CLICK, json) + } + + /** + * 事件ID:MessageItemClick + * 事件名称:消息点击事件 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param sessionMessageType 会话类型: 游戏消息、系统消息、客服消息、互动消息、运营消息 + * @param messageType 消息类型 + * @see EVENT_MESSAGE_ITEM_CLICK + */ + @JvmStatic + fun trackMessageItemClick( + gameId: String, + gameName: String, + sessionMessageType: String, + messageType: String + ) { + val json = json { + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + KEY_SESSION_MESSAGE_TYPE to sessionMessageType + KEY_MESSAGE_TYPE to messageType + } + + trackEvent(EVENT_MESSAGE_ITEM_CLICK, json) + } + + + /** + * 事件ID:MessageItemLinkClick + * 事件名称:消息点击事件 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param sessionMessageType 会话类型: 游戏消息、系统消息、客服消息、互动消息、运营消息 + * @param messageType 消息类型 + * @see EVENT_MESSAGE_ITEM_LINK_CLICK + */ + @JvmStatic + fun trackMessageItemLinkClick( + gameId: String, + gameName: String, + sessionMessageType: String, + messageType: String + ) { + val json = json { + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + KEY_SESSION_MESSAGE_TYPE to sessionMessageType + KEY_MESSAGE_TYPE to messageType + } + + trackEvent(EVENT_MESSAGE_ITEM_LINK_CLICK, json) + } + + /** + * 事件ID:LaunchServerSubscribeClick + * 事件名称:加入开服订阅事件 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @see EVENT_LAUNCH_SERVER_SUBSCRIBE_CLICK + */ + @JvmStatic + fun trackLaunchServerSubscribeClick( + gameId: String, + gameName: String + ) { + val json = json { + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + } + + trackEvent(EVENT_LAUNCH_SERVER_SUBSCRIBE_CLICK, json) + } + + /** + * 事件ID:LaunchServerSubscribeCancelClick + * 事件名称:取消开服订阅事件 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @see EVENT_LAUNCH_SERVER_SUBSCRIBE_CANCEL_CLICK + */ + @JvmStatic + fun trackLaunchServerSubscribeCancelClick( + gameId: String, + gameName: String + ) { + val json = json { + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + } + + trackEvent(EVENT_LAUNCH_SERVER_SUBSCRIBE_CANCEL_CLICK, json) + } + + /** + * 事件ID:LaunchServerReminderClick + * 事件名称:添加开服提醒事件 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @param reminderType 提醒类型: 已知服、未知服 + * @see EVENT_LAUNCH_SERVER_REMINDER_CLICK + */ + @JvmStatic + fun trackLaunchServerReminderClick( + gameId: String, + gameName: String, + reminderType: String + ) { + val json = json { + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + KEY_REMINDER_TYPE to reminderType + } + + trackEvent(EVENT_LAUNCH_SERVER_REMINDER_CLICK, json) + } + + /** + * 事件ID:LaunchServerReminderCancelClick + * 事件名称:游戏安装完成事件 + * @param gameId 游戏ID + * @param gameName 游戏名称 + * @see EVENT_LAUNCH_SERVER_REMINDER_CANCEL_CLICK + */ + @JvmStatic + fun trackLaunchServerReminderCancelClick( + gameId: String, + gameName: String, + reminderType: String + ) { + val json = json { + KEY_GAME_ID to gameId + KEY_GAME_NAME to gameName + KEY_REMINDER_TYPE to reminderType + } + + trackEvent(EVENT_LAUNCH_SERVER_REMINDER_CANCEL_CLICK, json) + } } \ No newline at end of file diff --git a/module_common/src/main/java/com/gh/gamecenter/common/utils/WXAPIProxyFactory.java b/module_common/src/main/java/com/gh/gamecenter/common/utils/WXAPIProxyFactory.java new file mode 100644 index 0000000000..1af346a6fe --- /dev/null +++ b/module_common/src/main/java/com/gh/gamecenter/common/utils/WXAPIProxyFactory.java @@ -0,0 +1,136 @@ +package com.gh.gamecenter.common.utils; + +import android.content.Context; +import android.content.Intent; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MediatorLiveData; +import androidx.lifecycle.MutableLiveData; + +import com.tencent.mm.opensdk.modelbase.BaseReq; +import com.tencent.mm.opensdk.modelbase.BaseResp; +import com.tencent.mm.opensdk.openapi.IWXAPI; +import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler; +import com.tencent.mm.opensdk.openapi.WXAPIFactory; +import com.tencent.mm.opensdk.utils.ILog; + +import java.util.Collections; +import java.util.List; + +/** + * 通用微信SDK代理工厂类 + * 1. 构造通用微信SDK代理类实例 + * 2. 处理WXEntryActivity的结果回调 + */ +public final class WXAPIProxyFactory { + private WXAPIProxyFactory() { + } + + private static final MutableLiveData mRespLiveData = new MutableLiveData<>(); + + public static WXAPIProxy createWXAPI(Context context, String appid) { + + final String transaction = String.valueOf(System.currentTimeMillis()); + + return createWXAPI(context, appid, new WXHandler() { + + @Override + public void handleOnReq(T req) { + req.transaction = transaction; + } + + @Override + public R handleOnResp(BaseResp resp) { + if (transaction.equals(resp.transaction)) { + return (R) resp; + } + return null; + } + }); + } + + public static WXAPIProxy createWXAPI(Context context, String appid, WXHandler handler) { + return new WXAPIProxy<>(context, appid, handler); + } + + public static MutableLiveData getLiveData() { + return mRespLiveData; + } + + public final static class WXAPIProxy { + + private final IWXAPI mWxAPI; + + private WXHandler mHandler; + + private final MediatorLiveData mLiveData = new MediatorLiveData() {{ + addSource(mRespLiveData, input -> { + R resp = mHandler.handleOnResp(input); + if (resp != null) { + mLiveData.setValue(resp); + } + }); + }}; + + private WXAPIProxy(Context context, String appid, WXHandler handler) { + mHandler = handler; + mWxAPI = WXAPIFactory.createWXAPI(context, appid); + } + + public boolean registerApp(String s) { + return mWxAPI.registerApp(s); + } + + public boolean registerApp(String s, long l) { + return mWxAPI.registerApp(s, l); + } + + public void unregisterApp() { + mWxAPI.unregisterApp(); + } + + public boolean handleIntent(Intent var1, IWXAPIEventHandler var2) { + return mWxAPI.handleIntent(var1, var2); + } + + public boolean isWXAppInstalled() { + return mWxAPI.isWXAppInstalled(); + } + + public int getWXAppSupportAPI() { + return mWxAPI.getWXAppSupportAPI(); + } + + public boolean openWXApp() { + return mWxAPI.openWXApp(); + } + + public boolean sendReq(T req) { + mHandler.handleOnReq(req); + return mWxAPI.sendReq(req); + } + + public boolean sendResp(R resp) { + return mWxAPI.sendResp(resp); + } + + public void detach() { + mWxAPI.detach(); + } + + public void setLogImpl(ILog var1) { + mWxAPI.setLogImpl(var1); + } + + public LiveData getLiveData() { + return mLiveData; + } + + } + + public interface WXHandler { + void handleOnReq(T req); + + R handleOnResp(BaseResp resp); + } +} diff --git a/app/src/main/res/drawable/bg_shape_f8_radius_8.xml b/module_common/src/main/res/drawable/bg_shape_f8_radius_8.xml similarity index 100% rename from app/src/main/res/drawable/bg_shape_f8_radius_8.xml rename to module_common/src/main/res/drawable/bg_shape_f8_radius_8.xml diff --git a/module_common/src/main/res/values-night/colors.xml b/module_common/src/main/res/values-night/colors.xml index 522d20e635..ff9f12b13d 100644 --- a/module_common/src/main/res/values-night/colors.xml +++ b/module_common/src/main/res/values-night/colors.xml @@ -93,6 +93,10 @@ #44AEEB + @color/theme + @color/theme_font + #0AFFFFFF + #05C400 #FFB13C diff --git a/module_common/src/main/res/values/colors.xml b/module_common/src/main/res/values/colors.xml index 95a10bed8b..53d2e041a1 100644 --- a/module_common/src/main/res/values/colors.xml +++ b/module_common/src/main/res/values/colors.xml @@ -103,6 +103,11 @@ @color/text_4BC7FF + + @color/theme + @color/theme_font + @color/black_alpha_7 + #05C400 #FFB13C diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MessageDigestEntity.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MessageDigestEntity.kt new file mode 100644 index 0000000000..8772e57246 --- /dev/null +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/MessageDigestEntity.kt @@ -0,0 +1,69 @@ +package com.gh.gamecenter.feature.entity + +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.IgnoredOnParcel + +data class MessageDigestEntity( + @SerializedName("_id") + var id: String = "", + @SerializedName("session_message_type") + var sessionMessageType: String = "", + @SerializedName("message_type") + var messageType: String = "", + @SerializedName("game_id") + var gameId: String = "", + @SerializedName("game_name") + var gameName: String = "" +) { + @IgnoredOnParcel + val messageTypeChinese: String + get() = when (messageType) { + "求加速回复" -> "求加速版本" + "求版本回复" -> "投票" + "comment_vote", + "answer_vote", + "answer_comment_vote", + "community_article_comment_vote", + "community_article_comment_reply_vote", + "community_article_vote", + "game_comment_vote", + "game_comment_reply_vote", + "video_vote", + "video_comment_vote", + "video_comment_reply_vote", + "game_list_comment_vote", + "game_list_vote", + "activity_comment_vote", + "activity_comment_reply_vote" -> "赞同" + "reply", + "answer", + "update-answer", + "follow_question", + "reply_answer_comment", + "answer_comment", + "community_article_comment", + "reply_community_article_comment", + "game_comment_reply", + "video_comment", + "video_comment_reply", + "game_list_comment", + "game_list_comment_reply", + "reply_activity_comment" -> "普通消息" + "system_invited", + "invited" -> "邀请" + "game_server_opening" -> "游戏开服日历" + else -> messageType + } + + @IgnoredOnParcel + val sessionMessageTypeChinese: String + get() = when (sessionMessageType) { + "system" -> "系统消息" + "user" -> "互动消息" + "announcement" -> "运营消息" + "service" -> "客服消息" + "game" -> "游戏消息" + "bbs" -> "论坛消息" + else -> sessionMessageType + } +} \ No newline at end of file diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarEntity.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarEntity.kt index 157b836363..25da966dfa 100644 --- a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarEntity.kt +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarEntity.kt @@ -9,9 +9,7 @@ import com.google.gson.annotations.SerializedName import java.text.SimpleDateFormat import java.util.* -/** - * Created by khy on 2017/3/17. - */ + class ServerCalendarEntity() : Parcelable, BaseObservable() { @SerializedName("_id") @@ -31,6 +29,9 @@ class ServerCalendarEntity() : Parcelable, BaseObservable() { var remark: String = "" + @SerializedName("notify_setting") + var notifySetting: ServerCalendarNotifySetting? = null + fun setNote(note: String?) { this.note = note notifyPropertyChanged(BR.note) @@ -97,6 +98,7 @@ class ServerCalendarEntity() : Parcelable, BaseObservable() { dest.writeLong(this.time) dest.writeString(this.type) dest.writeString(this.remark) + dest.writeParcelable(this.notifySetting, flags) } constructor(source: Parcel) : this() { @@ -105,6 +107,7 @@ class ServerCalendarEntity() : Parcelable, BaseObservable() { this.time = source.readLong() this.type = source.readString() this.remark = source.readString() ?: "" + this.notifySetting = source.readParcelable(ServerCalendarNotifySetting::class.java.classLoader) } companion object { diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarFormEntity.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarFormEntity.kt new file mode 100644 index 0000000000..569edaa667 --- /dev/null +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarFormEntity.kt @@ -0,0 +1,9 @@ +package com.gh.gamecenter.feature.entity + +import com.google.gson.annotations.SerializedName + +class ServerCalendarFormEntity( + @SerializedName("start_midnight") + val startMidnight: Long, + val games: List +) \ No newline at end of file diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarGame.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarGame.kt new file mode 100644 index 0000000000..3d42b4d41d --- /dev/null +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarGame.kt @@ -0,0 +1,52 @@ +package com.gh.gamecenter.feature.entity + +import com.gh.gamecenter.common.entity.IconFloat +import com.google.gson.annotations.SerializedName + +data class ServerCalendarGame( + @SerializedName("_id") + val id: String = "", + val name: String = "", + val category: String = "", + val brief: String = "", + val icon: String = "", + @SerializedName("ori_icon") + val oriIcon: String? = null, + @SerializedName("mirror_status") + val mirrorStatus: String = "off", + @SerializedName("_seq") + val seq: Int = 0, + @SerializedName("icon_float") + val iconFloat: IconFloat? = null, + @SerializedName("mirror_status2") + val mirrorStatus2: String = "off", + @SerializedName("icon_subscript") + var iconSubscript: String = "", + var subtitle: String? = "", + @SerializedName("subtitle_style") + var subtitleStyle: TagStyleEntity? = null, + val servers: List = emptyList(), + @SerializedName("servers_total") + val serversTotal: Int = 0, + @SerializedName("notify_status") + val notifyStatus: String? = null //todo 待提醒 coming 即将开服 opened 已开服 has_new 有新服 expired 已过期 +) { + + var startMidnight: Long = 0 + + fun toGameEntity(): GameEntity { + val gameEntity = GameEntity(id = id, name = name) + gameEntity.sequence = seq + gameEntity.icon = icon + gameEntity.rawIcon = oriIcon + gameEntity.iconSubscript = iconSubscript + gameEntity.mirrorStatus = mirrorStatus + gameEntity.mirrorStatus2 = mirrorStatus2 + gameEntity.subtitle = subtitle ?: "" + gameEntity.subtitleStyle = subtitleStyle + gameEntity.iconFloat = iconFloat + gameEntity.brief = brief + gameEntity.category = category + return gameEntity + } +} \ No newline at end of file diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarNotifySetting.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarNotifySetting.kt new file mode 100644 index 0000000000..4aa2f23e02 --- /dev/null +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/ServerCalendarNotifySetting.kt @@ -0,0 +1,25 @@ +package com.gh.gamecenter.feature.entity + +import android.os.Parcelable +import com.google.gson.annotations.SerializedName +import kotlinx.parcelize.Parcelize +import java.text.SimpleDateFormat +import java.util.* + +@Parcelize +class ServerCalendarNotifySetting( + @SerializedName("notify_time") + val notifyTime: Long = 0, + @SerializedName("seconds_advance") + val secondsAdvance: Int = 0, + @SerializedName("by_app") + val byApp: Boolean = false, + @SerializedName("by_wechat") + val byWechat: Boolean = false +) : Parcelable { + fun getFormatTime(pattern: String): String? { + if (notifyTime == 0L) return null + val formatTime = SimpleDateFormat(pattern, Locale.CHINA) + return formatTime.format(notifyTime * 1000) + } +} \ No newline at end of file diff --git a/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/WXSubscribeMsgConfig.kt b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/WXSubscribeMsgConfig.kt new file mode 100644 index 0000000000..b809b54525 --- /dev/null +++ b/module_core_feature/src/main/java/com/gh/gamecenter/feature/entity/WXSubscribeMsgConfig.kt @@ -0,0 +1,13 @@ +package com.gh.gamecenter.feature.entity + +import com.google.gson.annotations.SerializedName + +class WXSubscribeMsgConfig( + val appid: String, + val scene: Int, + @SerializedName("template_id") + val templateId: String, + @SerializedName("redirect_url") + val redirectUrl: String, + val reserved: String +) \ No newline at end of file diff --git a/module_login/src/main/java/com/gh/gamecenter/wxapi/WXEntryActivity.java b/module_login/src/main/java/com/gh/gamecenter/wxapi/WXEntryActivity.java index b7bc2ab61e..a27a496fb7 100644 --- a/module_login/src/main/java/com/gh/gamecenter/wxapi/WXEntryActivity.java +++ b/module_login/src/main/java/com/gh/gamecenter/wxapi/WXEntryActivity.java @@ -8,6 +8,7 @@ import android.widget.TextView; import com.alibaba.android.arouter.launcher.ARouter; import com.gh.gamecenter.common.utils.SensorsBridge; +import com.gh.gamecenter.common.utils.WXAPIProxyFactory; import com.gh.gamecenter.core.provider.IQGameProvider; import com.gh.gamecenter.login.utils.LoginHelper; import com.gh.gamecenter.common.constant.RouteConsts; @@ -100,6 +101,8 @@ public class WXEntryActivity extends Activity implements IWXAPIEventHandler, WeC if (iqGameProvider != null) { iqGameProvider.onWechatLogin(this, (SendAuth.Resp) baseResp); } + } else { + WXAPIProxyFactory.getLiveData().postValue(baseResp); } String resultString = ""; diff --git a/module_message/src/main/java/com/gh/gamecenter/message/entity/MessageGameEntity.kt b/module_message/src/main/java/com/gh/gamecenter/message/entity/MessageGameEntity.kt index 48c0f4ae50..b604d6a504 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/entity/MessageGameEntity.kt +++ b/module_message/src/main/java/com/gh/gamecenter/message/entity/MessageGameEntity.kt @@ -1,7 +1,6 @@ package com.gh.gamecenter.message.entity import com.gh.gamecenter.common.entity.LinkEntity -import com.gh.gamecenter.feature.entity.MessageEntity import com.gh.gamecenter.feature.entity.TimeEntity import com.gh.gamecenter.feature.entity.UserEntity import com.google.gson.annotations.SerializedName @@ -18,6 +17,8 @@ data class MessageGameEntity( ) { data class Content( var link: LinkEntity? = null, - var images: List = listOf() + var images: List = listOf(), + var type: String = "", + var lines: List> = emptyList() ) } \ No newline at end of file diff --git a/module_message/src/main/java/com/gh/gamecenter/message/utils/NewLogUtils.kt b/module_message/src/main/java/com/gh/gamecenter/message/utils/NewLogUtils.kt index db9e21a881..631b4471d8 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/utils/NewLogUtils.kt +++ b/module_message/src/main/java/com/gh/gamecenter/message/utils/NewLogUtils.kt @@ -51,6 +51,9 @@ object NewLogUtils { newsId: String, gameId: String, gameCollectionId: String, + sessionGameId: String, + sessionGameName: String, + sessionMessageType: String, messageType: String ) { val json = json { @@ -59,6 +62,9 @@ object NewLogUtils { "news_id" to newsId KEY_GAME_ID to gameId "game_collect_id" to gameCollectionId + "session_game_id" to sessionGameId + "session_game_name" to sessionGameName + "session_message_type" to sessionMessageType "message_type" to messageType parseAndPutMeta().invoke(this) } diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/KeFuFragmentAdapter.java b/module_message/src/main/java/com/gh/gamecenter/message/view/KeFuFragmentAdapter.java index 0408a78cd8..67fccd48a9 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/KeFuFragmentAdapter.java +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/KeFuFragmentAdapter.java @@ -197,9 +197,9 @@ public class KeFuFragmentAdapter extends ListAdapter { linkNewSkip(linkEntity); if ("求加速回复".equals(keFuEntity.getType())) { - NewLogUtils.logMessageInformClick("", "", "", "", "求加速版本"); + NewLogUtils.logMessageInformClick("", "", "", "", "", "", "", "求加速版本"); } else if ("求版本回复".equals(keFuEntity.getType())) { - NewLogUtils.logMessageInformClick("", "", "", "", "投票"); + NewLogUtils.logMessageInformClick("", "", "", "", "", "", "", "投票"); } }); } @@ -223,9 +223,9 @@ public class KeFuFragmentAdapter extends ListAdapter { linkSkip(link); if ("求加速回复".equals(keFuEntity.getType())) { - NewLogUtils.logMessageInformClick("", "", link.getId() == null ? "" : link.getId(), "", "求加速版本"); + NewLogUtils.logMessageInformClick("", "", link.getId() == null ? "" : link.getId(), "", "", "", "", "求加速版本"); } else if ("求版本回复".equals(keFuEntity.getType())) { - NewLogUtils.logMessageInformClick("", "", link.getId() == null ? "" : link.getId(), "", "投票"); + NewLogUtils.logMessageInformClick("", "", link.getId() == null ? "" : link.getId(), "", "", "", "", "投票"); } }); } diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/MessageFragment.kt b/module_message/src/main/java/com/gh/gamecenter/message/view/MessageFragment.kt index 98a91e06ba..13961416f5 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/MessageFragment.kt +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/MessageFragment.kt @@ -128,7 +128,8 @@ class MessageFragment : ListFragment() { entity, mEntrance, "消息_一级列表", - path + path, + "", "", "" ) if (!entity.read) { mListViewModel.postMessageRead(entity.id, entity.type) diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/MessageNormalFragment.java b/module_message/src/main/java/com/gh/gamecenter/message/view/MessageNormalFragment.java index aead04ea36..390ea8c4a8 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/MessageNormalFragment.java +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/MessageNormalFragment.java @@ -102,7 +102,7 @@ public class MessageNormalFragment extends ListFragment }); } - public static void messageItemClickSkip(View view, MessageEntity entity, String entrance, String outerInfo, String path) { + public static void messageItemClickSkip(View view, MessageEntity entity, String entrance, String outerInfo, String path, String sessionGameId, String sessionGameName, String sessionMessageType) { if (view == null || entity == null) return; Context context = view.getContext(); CommunityEntity community; @@ -641,6 +642,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder "", entity.getArticle().getId(), "", "", + sessionGameId, sessionGameName, sessionMessageType, + "赞同" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "赞同" ); if (view.getId() == R.id.message_original) { @@ -661,6 +669,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder "", entity.getArticle().getId(), "", "", + sessionGameId, sessionGameName, sessionMessageType, + "普通消息" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "普通消息" ); if (view.getId() == R.id.message_original) { @@ -680,6 +695,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getQuestion().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "邀请" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "邀请" ); if (view.getId() == R.id.message_original || view.getId() == R.id.message_item) { @@ -705,6 +727,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getQuestion().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + messageType + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, messageType ); ARouter.getInstance().build(RouteConsts.activity.questionDetailActivity) @@ -720,6 +749,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getQuestion().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "普通消息" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "普通消息" ); if (view.getId() == R.id.message_original) { @@ -742,6 +778,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getQuestion().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "普通消息" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "普通消息" ); if (newCommentDetailProvider != null) { @@ -765,6 +808,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getQuestion().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + messageType + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, messageType ); if (newCommentDetailProvider != null) { @@ -788,6 +838,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getArticle().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + messageType + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, messageType ); community = new CommunityEntity(entity.getArticle().getCommunityId(), ""); @@ -804,6 +861,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getArticle().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "赞同" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "赞同" ); if (newCommentDetailProvider != null) { @@ -822,6 +886,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getArticle().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "赞同" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "赞同" ); community = new CommunityEntity(entity.getArticle().getCommunityId(), ""); @@ -836,6 +907,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getArticle().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "普通消息" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "普通消息" ); if (newCommentDetailProvider != null) { @@ -855,6 +933,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder "", "", entity.getGame().getId(), "", + sessionGameId, sessionGameName, sessionMessageType, + "赞同" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "赞同" ); IGameDetailProvider gameDetailProvider = (IGameDetailProvider) ARouter.getInstance().build(RouteConsts.provider.gameDetail).navigation(); @@ -883,6 +968,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder "", "", entity.getGame().getId(), "", + sessionGameId, sessionGameName, sessionMessageType, + messageType + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, messageType ); ExposureSource exposureSource = new ExposureSource("消息中心", ""); @@ -901,6 +993,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getVideo().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "赞同" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "赞同" ); IDirectProvider directUtils = (IDirectProvider) ARouter.getInstance().build(RouteConsts.provider.directUtils).navigation(); @@ -921,6 +1020,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getVideo().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + messageType + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, messageType ); ARouter.getInstance().build(RouteConsts.activity.forumVideoDetailActivity) @@ -933,6 +1039,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getVideo().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "普通消息" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "普通消息" ); String commentId; @@ -956,6 +1069,13 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder NewLogUtils.logMessageInformClick( entity.getVideo().getId(), "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + "赞同" + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, "赞同" ); if (newCommentDetailProvider != null) { @@ -973,6 +1093,23 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder case "reply_activity_comment": case "activity_comment_vote": case "activity_comment_reply_vote": + if ("reply_activity_comment".equals(entity.getType())) { + messageType = "普通消息"; + } else { + messageType = "赞同"; + } + NewLogUtils.logMessageInformClick( + entity.getVideo().getId(), + "", "", "", + sessionGameId, sessionGameName, sessionMessageType, + messageType + ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, + messageType + ); IWebProvider webProvider = (IWebProvider) ARouter.getInstance().build(RouteConsts.provider.webActivity).navigation(); if (webProvider != null) { if (view.getId() == R.id.message_original) { @@ -986,9 +1123,16 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder case "game_list_comment": NewLogUtils.logMessageInformClick( "", "", "", + sessionGameId, sessionGameName, sessionMessageType, entity.getGameList().getId(), "普通消息" ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, + "普通消息" + ); if (gameCollectionDetailProvider != null) { context.startActivity(gameCollectionDetailProvider.getSpecifiedCommentIntent( context, @@ -1000,9 +1144,16 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder case "game_list_comment_vote": NewLogUtils.logMessageInformClick( "", "", "", + sessionGameId, sessionGameName, sessionMessageType, entity.getGameList().getId(), "赞同" ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, + "赞同" + ); if (gameCollectionDetailProvider != null && newCommentDetailProvider != null) { if (TextUtils.isEmpty(entity.getComment().getTopId())) { intent = gameCollectionDetailProvider.getSpecifiedCommentIntent( @@ -1026,9 +1177,16 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder case "game_list_comment_reply": NewLogUtils.logMessageInformClick( "", "", "", + sessionGameId, sessionGameName, sessionMessageType, entity.getGameList().getId(), "普通消息" ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, + "普通消息" + ); if (newCommentDetailProvider != null) { context.startActivity(newCommentDetailProvider.getGameCollectionCommentIntent( context, @@ -1044,9 +1202,16 @@ public class MessageItemViewHolder extends BaseRecyclerViewHolder case "game_list_vote": NewLogUtils.logMessageInformClick( "", "", "", + sessionGameId, sessionGameName, sessionMessageType, entity.getGameList().getId(), "赞同" ); + SensorsBridge.trackMessageItemClick( + sessionGameId, + sessionGameName, + sessionMessageType, + "赞同" + ); if (gameCollectionDetailProvider != null) { context.startActivity(gameCollectionDetailProvider.getIntent( context, diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListActivity.kt b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListActivity.kt index 4f272d6a4a..75aa76b699 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListActivity.kt +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListActivity.kt @@ -24,11 +24,12 @@ class MessageListActivity : ToolBarActivity() { } companion object { - fun getIntent(context: Context, type: String, name: String, gameId: String, entrance: String): Intent { + fun getIntent(context: Context, type: String, name: String, gameId: String, gameName: String, entrance: String): Intent { val bundle = Bundle() bundle.putString(EntranceConsts.KEY_TYPE, type) bundle.putString(EntranceConsts.KEY_NAME, name) bundle.putString(EntranceConsts.KEY_GAMEID, gameId) + bundle.putString(EntranceConsts.KEY_GAMENAME, gameName) bundle.putString(EntranceConsts.KEY_ENTRANCE, entrance) return getTargetIntent( context, diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListAdapter.kt b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListAdapter.kt index 2713a75710..c59992e1fe 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListAdapter.kt +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListAdapter.kt @@ -6,6 +6,7 @@ import android.text.Html import android.text.TextUtils import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams import android.widget.LinearLayout import android.widget.TextView import androidx.core.view.isVisible @@ -31,6 +32,7 @@ import com.gh.gamecenter.feature.provider.ILinkDirectUtilsProvider import com.gh.gamecenter.feature.provider.ISubjectProvider import com.gh.gamecenter.login.user.UserManager import com.gh.gamecenter.message.R +import com.gh.gamecenter.message.databinding.MessageLineItemBinding import com.gh.gamecenter.message.entity.MessageGameEntity import com.gh.gamecenter.message.entity.MessageItemData import com.gh.gamecenter.message.entity.MessageKeFuEntity @@ -41,6 +43,8 @@ import com.lightgame.utils.Utils class MessageListAdapter( context: Context, + private val mGameId: String, + private val mGameName: String, private val mType: String, private val mListViewModel: MessageListViewModel, private val mEntrance: String @@ -70,9 +74,8 @@ class MessageListAdapter( MessageItemViewHolder.messageItemClickSkip( view, data as MessageEntity, - "", - "", - "" + "", "", "", + mGameId, mGameName, mType ) } }, @@ -196,9 +199,21 @@ class MessageListAdapter( } linkNewSkip(linkEntity) if ("求加速回复" == entity.type) { - com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick("", "", "", "", "求加速版本") + com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick("", "", "", "", mGameId, mGameName, mType, "求加速版本") + SensorsBridge.trackMessageItemClick( + gameId = mGameId, + gameName = mGameName, + sessionMessageType = mType, + messageType = "求加速回复" + ) } else if ("求版本回复" == entity.type) { - com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick("", "", "", "", "投票") + com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick("", "", "", "", mGameId, mGameName, mType, "投票") + SensorsBridge.trackMessageItemClick( + gameId = mGameId, + gameName = mGameName, + sessionMessageType = mType, + messageType = "投票" + ) } } } @@ -224,12 +239,28 @@ class MessageListAdapter( if ("求加速回复" == entity.type) { com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick( "", "", - (if (link.id == null) "" else link.id)!!, "", "求加速版本" + (if (link.id == null) "" else link.id)!!, "", + mGameId, mGameName, mType, + "求加速版本" + ) + SensorsBridge.trackMessageItemClick( + gameId = mGameId, + gameName = mGameName, + sessionMessageType = mType, + messageType = "求加速版本" ) } else if ("求版本回复" == entity.type) { com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick( "", "", - (if (link.id == null) "" else link.id)!!, "", "投票" + (if (link.id == null) "" else link.id)!!, "", + mGameId, mGameName, mType, + "投票" + ) + SensorsBridge.trackMessageItemClick( + gameId = mGameId, + gameName = mGameName, + sessionMessageType = mType, + messageType = "投票" ) } } @@ -317,6 +348,32 @@ class MessageListAdapter( spannableText.copyTextAndToast("已复制:$spannableText") } + if (entity.content?.type == "game_server_opening" && entity.content?.lines?.isNotEmpty() == true) { + messageLinesContainer.removeAllViews() + messageLinesContainer.visibility = View.VISIBLE + val lines = if (entity.content!!.lines.size > 3) { + entity.content!!.lines.subList(0, 3) + } else { + entity.content!!.lines + } + lines.forEachIndexed { index, line -> + MessageLineItemBinding.inflate( + mLayoutInflater, + messageLinesContainer, + true + ).apply { + ImageUtils.display(icon, line[0]) + date.text = line[1] + serverName.text = line[2] + root.layoutParams = (root.layoutParams as MarginLayoutParams).apply { + bottomMargin = if (index < lines.size - 1) 10F.dip2px() else 0 + } + } + } + } else { + messageLinesContainer.visibility = View.GONE + } + if (!entity.content?.images.isNullOrEmpty()) { holder.binding.messageKefuImagesContainer.removeAllViews() holder.binding.messageKefuImagesContainer.visibility = View.VISIBLE @@ -369,10 +426,38 @@ class MessageListAdapter( "", "", "", + mGameId, mGameName, mType, "求加速版本" ) + SensorsBridge.trackMessageItemClick( + gameId = mGameId, + gameName = mGameName, + sessionMessageType = mType, + messageType = "求加速版本" + ) } else if ("求版本回复" == entity.type) { - com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick("", "", "", "", "投票") + com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick("", "", "", "", mGameId, mGameName, mType, "投票") + SensorsBridge.trackMessageItemClick( + gameId = mGameId, + gameName = mGameName, + sessionMessageType = mType, + messageType = "投票" + ) + } else if ("game_server_calendar" == linkEntity.type) { + com.gh.gamecenter.message.utils.NewLogUtils.logMessageInformClick( + "", + "", + "", + "", + mGameId, mGameName, mType, + "游戏开服日历" + ) + SensorsBridge.trackMessageItemClick( + gameId = mGameId, + gameName = mGameName, + sessionMessageType = mType, + messageType = "游戏开服日历" + ) } } } @@ -578,7 +663,6 @@ class MessageListAdapter( .withString(EntranceConsts.KEY_ENTRANCE, "消息中心-系统消息") .navigation() } - else -> { var exposureEvent: ExposureEvent? = null if (type == "游戏" || type == "game") { diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListFragment.kt b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListFragment.kt index 0f6d0c6f99..35c3479da7 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListFragment.kt +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/message/MessageListFragment.kt @@ -14,10 +14,11 @@ class MessageListFragment : ListFragment( private var mAdapter: MessageListAdapter? = null private var mType = "" private var mGameId = "" + private var mGameName = "" override fun provideListAdapter(): MessageListAdapter { if (mAdapter == null) { - mAdapter = MessageListAdapter(requireContext(), mType, mViewModel, mEntrance) + mAdapter = MessageListAdapter(requireContext(), mGameId, mGameName, mType, mViewModel, mEntrance) } return mAdapter as MessageListAdapter } @@ -30,6 +31,7 @@ class MessageListFragment : ListFragment( override fun onCreate(savedInstanceState: Bundle?) { mType = requireArguments().getString(EntranceConsts.KEY_TYPE, "") mGameId = requireArguments().getString(EntranceConsts.KEY_GAMEID, "") + mGameName = requireArguments().getString(EntranceConsts.KEY_GAMENAME, "") super.onCreate(savedInstanceState) setNavigationTitle(requireArguments().getString(EntranceConsts.KEY_NAME, "")) } diff --git a/module_message/src/main/java/com/gh/gamecenter/message/view/message/SortedMessageListAdapter.kt b/module_message/src/main/java/com/gh/gamecenter/message/view/message/SortedMessageListAdapter.kt index 201314298c..dd745c639b 100644 --- a/module_message/src/main/java/com/gh/gamecenter/message/view/message/SortedMessageListAdapter.kt +++ b/module_message/src/main/java/com/gh/gamecenter/message/view/message/SortedMessageListAdapter.kt @@ -118,6 +118,11 @@ class SortedMessageListAdapter( entity.unreadCount, "点击进入消息列表" ) + SensorsBridge.trackMessageSessionClick( + gameId = gameId, + gameName = gameName, + sessionMessageType = entity.typeChinese + ) if (entity.unreadCount > 0) { mViewModel.markSortedMessageRead(position, entity.id) } @@ -127,6 +132,7 @@ class SortedMessageListAdapter( entity.type, entity.title, gameId, + gameName, BaseActivity.mergeEntranceAndPath(mEntrance, "二级列表") ) ) diff --git a/module_message/src/main/res/layout/message_kefu_item.xml b/module_message/src/main/res/layout/message_kefu_item.xml index 30d42c68fa..c9fd5d7d3e 100644 --- a/module_message/src/main/res/layout/message_kefu_item.xml +++ b/module_message/src/main/res/layout/message_kefu_item.xml @@ -117,6 +117,21 @@ android:visibility="visible" tools:text="Kris is here to help" /> + + + + + + + + + + + \ No newline at end of file diff --git a/module_sensors_data/src/main/java/com/gh/gamecenter/sensorsdata/SensorsHelper.kt b/module_sensors_data/src/main/java/com/gh/gamecenter/sensorsdata/SensorsHelper.kt index 16b560ce5c..dcc248e9af 100644 --- a/module_sensors_data/src/main/java/com/gh/gamecenter/sensorsdata/SensorsHelper.kt +++ b/module_sensors_data/src/main/java/com/gh/gamecenter/sensorsdata/SensorsHelper.kt @@ -131,7 +131,7 @@ object SensorsHelper { @JvmStatic fun trackEvent(eventName: String, jsonObject: JSONObject) { try { - Utils.log("SensorHelper", "eventName:$eventName\n ${jsonObject.toString(4)}") + Utils.log("SensorsHelper", "eventName:$eventName\n ${jsonObject.toString(4)}") SensorsDataAPI.sharedInstance().track(eventName, jsonObject) } catch (e: JSONException) { e.printStackTrace()