diff --git a/.github/workflows/holiday-generator/main.js b/.github/workflows/holiday-generator/main.js index 26b77b95c..1de710ea3 100644 --- a/.github/workflows/holiday-generator/main.js +++ b/.github/workflows/holiday-generator/main.js @@ -28,6 +28,7 @@ function log(toLog) { */ function getEvents(countryCode) { const generator = new Holidays(countryCode); + generator.setTimezone("UTC"); const events = []; for (let i = START_YEAR; i <= END_YEAR; i++) { events.push(...generator.getHolidays(i).filter((x) => TYPE_WHITELIST.includes(x.type))); @@ -54,7 +55,7 @@ function generateUid(countryCode, date, rule) { * @returns */ function getDateArray(date) { - return [date.getFullYear(), date.getMonth() + 1, date.getDate()]; + return [date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate()]; } /** @@ -79,9 +80,9 @@ async function generateIcal(events, countryCode) { if (isFixedDate(x.rule)) { const uid = generateUid(countryCode, "", x.rule); if (!eventsMap.has(uid)) { - const yearDiff = x.end.getFullYear() - x.start.getFullYear(); - x.start.setFullYear(FIXED_DATE_START_YEAR); - x.end.setFullYear(FIXED_DATE_START_YEAR + yearDiff); + const yearDiff = x.end.getUTCFullYear() - x.start.getUTCFullYear(); + x.start.setUTCFullYear(FIXED_DATE_START_YEAR); + x.end.setUTCFullYear(FIXED_DATE_START_YEAR + yearDiff); eventsMap.set(uid, { title: x.name, uid, diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml index eef815bdc..65ae5a77a 100644 --- a/.github/workflows/no-response.yml +++ b/.github/workflows/no-response.yml @@ -21,5 +21,5 @@ jobs: with: token: ${{ github.token }} # Number of days of inactivity before an issue is closed for lack of response. - daysUntilClose: 30 + daysUntilClose: 14 responseRequiredLabel: waiting for author diff --git a/app/src/main/assets/bangladesh.ics b/app/src/main/assets/bangladesh.ics new file mode 100644 index 000000000..857dc9d6f --- /dev/null +++ b/app/src/main/assets/bangladesh.ics @@ -0,0 +1,128 @@ +BEGIN:VCALENDAR +BEGIN:VEVENT +UID:bgd_1 +DTSTART;VALUE=DATE:20250221 +DTEND;VALUE=DATE:20250222 +RRULE:FREQ=YEARLY;INTERVAL=1 +SUMMARY:শহীদ দিবস +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_2 +DTSTART;VALUE=DATE:20250317 +DTEND;VALUE=DATE:20250318 +RRULE:FREQ=YEARLY;INTERVAL=1 +SUMMARY:শেখ মুজিবুর রহমানের জন্মদিন +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_3 +DTSTART;VALUE=DATE:20250326 +DTEND;VALUE=DATE:20250327 +RRULE:FREQ=YEARLY;INTERVAL=1 +SUMMARY:স্বাধীনতা দিবস +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_4 +DTSTART;VALUE=DATE:20250414 +DTEND;VALUE=DATE:20250415 +RRULE:FREQ=YEARLY;INTERVAL=1 +SUMMARY:পহেলা বৈশাখ +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_5 +DTSTART;VALUE=DATE:20250501 +DTEND;VALUE=DATE:20250502 +RRULE:FREQ=YEARLY;INTERVAL=1 +SUMMARY:মে দিবস +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_6 +DTSTART;VALUE=DATE:20241216 +DTEND;VALUE=DATE:20241217 +RRULE:FREQ=YEARLY;INTERVAL=1 +SUMMARY:বিজয় দিবস +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_7 +DTSTART;VALUE=DATE:20241225 +DTEND;VALUE=DATE:20241226 +RRULE:FREQ=YEARLY;INTERVAL=1 +SUMMARY:বড়দিন +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_8 +DTSTART;VALUE=DATE:20250214 +DTEND;VALUE=DATE:20250215 +SUMMARY:শবে বরাত +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_9 +DTSTART;VALUE=DATE:20250327 +DTEND;VALUE=DATE:20250328 +SUMMARY:শবে কদর +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_10 +DTSTART;VALUE=DATE:20250328 +DTEND;VALUE=DATE:20250329 +SUMMARY:জুমাতুল বিদা +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_11 +DTSTART;VALUE=DATE:20250331 +DTEND;VALUE=DATE:20250403 +SUMMARY:ঈদুল ফিতর +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_12 +DTSTART;VALUE=DATE:20250505 +DTEND;VALUE=DATE:20250506 +SUMMARY:বুদ্ধ পূর্ণিমা +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_13 +DTSTART;VALUE=DATE:20250606 +DTEND;VALUE=DATE:20250509 +SUMMARY:ঈদুল আজহা +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_14 +DTSTART;VALUE=DATE:20250706 +DTEND;VALUE=DATE:20250707 +SUMMARY:আশুরা +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_15 +DTSTART;VALUE=DATE:20250816 +DTEND;VALUE=DATE:20250817 +SUMMARY:জন্মাষ্টমী +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_16 +DTSTART;VALUE=DATE:20250905 +DTEND;VALUE=DATE:20250906 +SUMMARY:ঈদে মিলাদুন্নবী +STATUS:CONFIRMED +END:VEVENT +BEGIN:VEVENT +UID:bgd_17 +DTSTART;VALUE=DATE:20251002 +DTEND;VALUE=DATE:20251003 +SUMMARY:বিজয়া দশমী +STATUS:CONFIRMED +END:VEVENT +END:VCALENDAR diff --git a/app/src/main/kotlin/org/fossify/calendar/activities/MainActivity.kt b/app/src/main/kotlin/org/fossify/calendar/activities/MainActivity.kt index dabff20b5..ff8223820 100644 --- a/app/src/main/kotlin/org/fossify/calendar/activities/MainActivity.kt +++ b/app/src/main/kotlin/org/fossify/calendar/activities/MainActivity.kt @@ -415,11 +415,13 @@ class MainActivity : SimpleActivity(), RefreshRecyclerViewListener { val eventIdToOpen = intent.getLongExtra(EVENT_ID, 0L) val eventOccurrenceToOpen = intent.getLongExtra(EVENT_OCCURRENCE_TS, 0L) + val isTask = intent.getBooleanExtra(IS_TASK, false) intent.removeExtra(EVENT_ID) intent.removeExtra(EVENT_OCCURRENCE_TS) + intent.removeExtra(IS_TASK) if (eventIdToOpen != 0L && eventOccurrenceToOpen != 0L) { hideKeyboard() - Intent(this, EventActivity::class.java).apply { + Intent(this, getActivityToOpen(isTask)).apply { putExtra(EVENT_ID, eventIdToOpen) putExtra(EVENT_OCCURRENCE_TS, eventOccurrenceToOpen) startActivity(this) diff --git a/app/src/main/kotlin/org/fossify/calendar/activities/SplashActivity.kt b/app/src/main/kotlin/org/fossify/calendar/activities/SplashActivity.kt index 2721e85b6..305e67a45 100644 --- a/app/src/main/kotlin/org/fossify/calendar/activities/SplashActivity.kt +++ b/app/src/main/kotlin/org/fossify/calendar/activities/SplashActivity.kt @@ -18,6 +18,7 @@ class SplashActivity : BaseSplashActivity() { intent.extras?.containsKey(EVENT_ID) == true -> Intent(this, MainActivity::class.java).apply { putExtra(EVENT_ID, intent.getLongExtra(EVENT_ID, 0L)) putExtra(EVENT_OCCURRENCE_TS, intent.getLongExtra(EVENT_OCCURRENCE_TS, 0L)) + putExtra(IS_TASK, intent.getBooleanExtra(IS_TASK, false)) startActivity(this) } diff --git a/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt b/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt index b0a5334c3..a674a2faf 100644 --- a/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt +++ b/app/src/main/kotlin/org/fossify/calendar/adapters/EventListWidgetAdapter.kt @@ -98,7 +98,7 @@ class EventListWidgetAdapter(val context: Context, val intent: Intent) : RemoteV setText(R.id.event_item_time, "$timeText\n$descriptionText") } - if (item.isTask && item.isTaskCompleted && dimCompletedTasks || dimPastEvents && item.isPastEvent) { + if (item.isTask && item.isTaskCompleted && dimCompletedTasks || dimPastEvents && item.isPastEvent && !item.isTask) { curTextColor = weakTextColor } @@ -126,6 +126,7 @@ class EventListWidgetAdapter(val context: Context, val intent: Intent) : RemoteV Intent().apply { putExtra(EVENT_ID, item.id) putExtra(EVENT_OCCURRENCE_TS, item.startTS) + putExtra(IS_TASK, item.isTask) setOnClickFillInIntent(R.id.event_item_holder, this) } } diff --git a/app/src/main/kotlin/org/fossify/calendar/adapters/ManageEventTypesAdapter.kt b/app/src/main/kotlin/org/fossify/calendar/adapters/ManageEventTypesAdapter.kt index 73f47df2e..63032913f 100644 --- a/app/src/main/kotlin/org/fossify/calendar/adapters/ManageEventTypesAdapter.kt +++ b/app/src/main/kotlin/org/fossify/calendar/adapters/ManageEventTypesAdapter.kt @@ -138,7 +138,7 @@ class ManageEventTypesAdapter( private fun askConfirmDelete() { val eventTypes = eventTypes.filter { selectedKeys.contains(it.id?.toInt()) }.map { it.id } as ArrayList - activity.eventsHelper.doEventTypesContainEvents(eventTypes) { + activity.eventsHelper.doEventTypesContainEventsOrTasks(eventTypes) { activity.runOnUiThread { if (it) { val res = activity.resources diff --git a/app/src/main/kotlin/org/fossify/calendar/fragments/WeekFragment.kt b/app/src/main/kotlin/org/fossify/calendar/fragments/WeekFragment.kt index db45bd9ff..9f1a6be0c 100644 --- a/app/src/main/kotlin/org/fossify/calendar/fragments/WeekFragment.kt +++ b/app/src/main/kotlin/org/fossify/calendar/fragments/WeekFragment.kt @@ -755,8 +755,7 @@ class WeekFragment : Fragment(), WeeklyCalendar { private fun shouldAddEventOnTopBar(isAllDay: Boolean, startDayCode: String, endDayCode: String): Boolean { val spansMultipleDays = startDayCode != endDayCode - val isSingleDayAllDayEvent = isAllDay && !spansMultipleDays - return isSingleDayAllDayEvent || (spansMultipleDays && config.showMidnightSpanningEventsAtTop) + return isAllDay || (spansMultipleDays && config.showMidnightSpanningEventsAtTop) } @SuppressLint("NewApi") diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt index b6d9b24f8..76434d0a5 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/Constants.kt @@ -40,6 +40,7 @@ const val SHORTCUT_NEW_TASK = "shortcut_new_task" const val REGULAR_EVENT_TYPE_ID = 1L const val TIME_ZONE = "time_zone" const val CURRENT_TIME_ZONE = "current_time_zone" +const val IS_TASK = "is_task" const val MONTHLY_VIEW = 1 const val YEARLY_VIEW = 2 diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt index 83dc45851..d7faba95d 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt @@ -97,9 +97,9 @@ class EventsHelper(val context: Context) { for (eventTypeId in deleteIds) { if (deleteEvents) { - deleteEventsWithType(eventTypeId!!) + deleteEventsAndTasksWithType(eventTypeId!!) } else { - eventsDB.resetEventsWithType(eventTypeId!!) + eventsDB.resetEventsAndTasksWithType(eventTypeId!!) } } @@ -291,8 +291,8 @@ class EventsHelper(val context: Context) { } } - private fun deleteEventsWithType(eventTypeId: Long) { - val eventIds = eventsDB.getEventIdsByEventType(eventTypeId).toMutableList() + private fun deleteEventsAndTasksWithType(eventTypeId: Long) { + val eventIds = eventsDB.getEventAndTasksIdsByEventType(eventTypeId).toMutableList() deleteEvents(eventIds, true) } @@ -317,9 +317,9 @@ class EventsHelper(val context: Context) { } } - fun doEventTypesContainEvents(eventTypeIds: ArrayList, callback: (contain: Boolean) -> Unit) { + fun doEventTypesContainEventsOrTasks(eventTypeIds: ArrayList, callback: (contain: Boolean) -> Unit) { ensureBackgroundThread { - val eventIds = eventsDB.getEventIdsByEventType(eventTypeIds) + val eventIds = eventsDB.getEventAndTasksIdsByEventType(eventTypeIds) callback(eventIds.isNotEmpty()) } } diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/MyTimeZones.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/MyTimeZones.kt index f2a569f5f..e5c0fd315 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/MyTimeZones.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/MyTimeZones.kt @@ -367,7 +367,7 @@ fun getAllTimeZones() = arrayListOf( MyTimeZone("GMT+5:30", "Asia/Kolkata"), MyTimeZone("GMT+5:45", "Asia/Kathmandu"), MyTimeZone("GMT+6", "Antarctica/Vostok"), - MyTimeZone("GMT+6", "Asia/Almaty"), + MyTimeZone("GMT+5", "Asia/Almaty"), MyTimeZone("GMT+6", "Asia/Bishkek"), MyTimeZone("GMT+6", "Asia/Dhaka"), MyTimeZone("GMT+6", "Asia/Omsk"), diff --git a/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetMonthlyProvider.kt b/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetMonthlyProvider.kt index 61bbabcb9..33e26a381 100644 --- a/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetMonthlyProvider.kt +++ b/app/src/main/kotlin/org/fossify/calendar/helpers/MyWidgetMonthlyProvider.kt @@ -140,7 +140,7 @@ class MyWidgetMonthlyProvider : AppWidgetProvider() { val backgroundColor = it.color var eventTextColor = backgroundColor.getContrastColor() - if (it.isTask() && it.isTaskCompleted() && dimCompletedTasks || !day.isThisMonth || (dimPastEvents && it.isPastEvent)) { + if (it.isTask() && it.isTaskCompleted() && dimCompletedTasks || !day.isThisMonth || (dimPastEvents && it.isPastEvent && !it.isTask())) { eventTextColor = eventTextColor.adjustAlpha(MEDIUM_ALPHA) } diff --git a/app/src/main/kotlin/org/fossify/calendar/interfaces/EventsDao.kt b/app/src/main/kotlin/org/fossify/calendar/interfaces/EventsDao.kt index b99b12c74..a0c17d1c2 100644 --- a/app/src/main/kotlin/org/fossify/calendar/interfaces/EventsDao.kt +++ b/app/src/main/kotlin/org/fossify/calendar/interfaces/EventsDao.kt @@ -97,11 +97,11 @@ interface EventsDao { @Query("SELECT id FROM events WHERE import_id LIKE :importId AND type = $TYPE_EVENT") fun getEventIdWithLastImportId(importId: String): Long? - @Query("SELECT id FROM events WHERE event_type = :eventTypeId AND type = $TYPE_EVENT") - fun getEventIdsByEventType(eventTypeId: Long): List + @Query("SELECT id FROM events WHERE event_type = :eventTypeId") + fun getEventAndTasksIdsByEventType(eventTypeId: Long): List - @Query("SELECT id FROM events WHERE event_type IN (:eventTypeIds) AND type = $TYPE_EVENT") - fun getEventIdsByEventType(eventTypeIds: List): List + @Query("SELECT id FROM events WHERE event_type IN (:eventTypeIds)") + fun getEventAndTasksIdsByEventType(eventTypeIds: List): List @Query("SELECT id FROM events WHERE parent_id IN (:parentIds)") fun getEventIdsWithParentIds(parentIds: List): List @@ -109,8 +109,8 @@ interface EventsDao { @Query("SELECT id FROM events WHERE source = :source AND import_id != \"\" AND type = $TYPE_EVENT") fun getCalDAVCalendarEvents(source: String): List - @Query("UPDATE events SET event_type = $REGULAR_EVENT_TYPE_ID WHERE event_type = :eventTypeId AND type = $TYPE_EVENT") - fun resetEventsWithType(eventTypeId: Long) + @Query("UPDATE events SET event_type = $REGULAR_EVENT_TYPE_ID WHERE event_type = :eventTypeId") + fun resetEventsAndTasksWithType(eventTypeId: Long) @Query("UPDATE events SET import_id = :importId, source = :source WHERE id = :id AND type = $TYPE_EVENT") fun updateEventImportIdAndSource(importId: String, source: String, id: Long) diff --git a/app/src/main/kotlin/org/fossify/calendar/views/MonthViewWrapper.kt b/app/src/main/kotlin/org/fossify/calendar/views/MonthViewWrapper.kt index 321512421..415161552 100644 --- a/app/src/main/kotlin/org/fossify/calendar/views/MonthViewWrapper.kt +++ b/app/src/main/kotlin/org/fossify/calendar/views/MonthViewWrapper.kt @@ -4,17 +4,29 @@ import android.content.Context import android.util.AttributeSet import android.view.LayoutInflater import android.widget.FrameLayout +import org.fossify.calendar.R import org.fossify.calendar.databinding.MonthViewBackgroundBinding import org.fossify.calendar.databinding.MonthViewBinding import org.fossify.calendar.extensions.config +import org.fossify.calendar.extensions.launchNewEventIntent +import org.fossify.calendar.extensions.launchNewTaskIntent import org.fossify.calendar.helpers.COLUMN_COUNT import org.fossify.calendar.helpers.Formatter import org.fossify.calendar.helpers.ROW_COUNT +import org.fossify.calendar.helpers.TYPE_EVENT +import org.fossify.calendar.helpers.TYPE_TASK import org.fossify.calendar.models.DayMonthly +import org.fossify.commons.compose.extensions.getActivity +import org.fossify.commons.dialogs.RadioGroupDialog import org.fossify.commons.extensions.onGlobalLayout +import org.fossify.commons.models.RadioItem // used in the Monthly view fragment, 1 view per screen -class MonthViewWrapper(context: Context, attrs: AttributeSet, defStyle: Int) : FrameLayout(context, attrs, defStyle) { +class MonthViewWrapper( + context: Context, + attrs: AttributeSet, + defStyle: Int +) : FrameLayout(context, attrs, defStyle) { private var dayWidth = 0f private var dayHeight = 0f private var weekDaysLetterHeight = 0 @@ -29,7 +41,8 @@ class MonthViewWrapper(context: Context, attrs: AttributeSet, defStyle: Int) : F constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) init { - val normalTextSize = resources.getDimensionPixelSize(org.fossify.commons.R.dimen.normal_text_size).toFloat() + val normalTextSize = + resources.getDimensionPixelSize(org.fossify.commons.R.dimen.normal_text_size).toFloat() weekDaysLetterHeight = 2 * normalTextSize.toInt() inflater = LayoutInflater.from(context) @@ -72,7 +85,12 @@ class MonthViewWrapper(context: Context, attrs: AttributeSet, defStyle: Int) : F val childRight = childLeft + childWidth val childBottom = childTop + childHeight - child.layout(childLeft.toInt(), childTop.toInt(), childRight.toInt(), childBottom.toInt()) + child.layout( + childLeft.toInt(), + childTop.toInt(), + childRight.toInt(), + childBottom.toInt() + ) if (curLeft + childWidth <= end) { curLeft += childWidth @@ -85,7 +103,11 @@ class MonthViewWrapper(context: Context, attrs: AttributeSet, defStyle: Int) : F } } - fun updateDays(newDays: ArrayList, addEvents: Boolean, callback: ((DayMonthly) -> Unit)? = null) { + fun updateDays( + newDays: ArrayList, + addEvents: Boolean, + callback: ((DayMonthly) -> Unit)? = null + ) { setupHorizontalOffset() measureSizes() dayClickCallback = callback @@ -128,7 +150,12 @@ class MonthViewWrapper(context: Context, attrs: AttributeSet, defStyle: Int) : F background = null } //Accessible label composed by day and month - contentDescription = "${day.value} ${Formatter.getMonthName(context, Formatter.getDateTimeFromCode(day.code).monthOfYear)}" + contentDescription = "${day.value} ${ + Formatter.getMonthName( + context, + Formatter.getDateTimeFromCode(day.code).monthOfYear + ) + }" setOnClickListener { dayClickCallback?.invoke(day) @@ -138,6 +165,26 @@ class MonthViewWrapper(context: Context, attrs: AttributeSet, defStyle: Int) : F } } + setOnLongClickListener { + if (context.config.allowCreatingTasks) { + val items = arrayListOf( + RadioItem(TYPE_EVENT, context.getString(R.string.event)), + RadioItem(TYPE_TASK, context.getString(R.string.task)) + ) + + RadioGroupDialog(context.getActivity(), items) { + if (it == TYPE_EVENT) { + context.launchNewEventIntent(day.code) + } else { + context.launchNewTaskIntent(day.code) + } + } + } else { + context.launchNewEventIntent(day.code) + } + true + } + addView(this) } } diff --git a/app/src/main/res/layout/widget_date.xml b/app/src/main/res/layout/widget_date.xml index 89f4f8bee..c2e343ab6 100644 --- a/app/src/main/res/layout/widget_date.xml +++ b/app/src/main/res/layout/widget_date.xml @@ -9,40 +9,56 @@ android:id="@+id/widget_date_background" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignStart="@+id/widget_date" - android:layout_alignTop="@+id/widget_date" - android:layout_alignEnd="@+id/widget_date" - android:layout_alignBottom="@+id/widget_month" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:layout_alignParentEnd="true" + android:layout_alignParentBottom="true" android:src="@drawable/widget_round_background" tools:ignore="ContentDescription" /> - + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingLeft="@dimen/small_margin" + android:paddingRight="@dimen/small_margin" + android:paddingBottom="@dimen/small_margin"> - + + + +