Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for ICS COLOR property (#188) #421

Merged
merged 4 commits into from
Jan 19, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ const val LOCATION = "LOCATION"
const val RECURRENCE_ID = "RECURRENCE-ID"
const val SEQUENCE = "SEQUENCE"
const val TZID = "TZID"
const val COLOR = "COLOR:"
const val FOSSIFY_COLOR = "X-FOSSIFY-EVENT-COLOR:"

// this tag isn't a standard ICS tag, but there's no official way of adding a category color in an ics file
const val SMT_CATEGORY_COLOR = "X-SMT-CATEGORY-COLOR:"
Expand Down Expand Up @@ -349,4 +351,4 @@ fun getStatusStringFromEventStatus(statusCode: Int): String {
Events.STATUS_CANCELED -> CANCELLED
else -> TENTATIVE
}
}
}
182 changes: 182 additions & 0 deletions app/src/main/kotlin/org/fossify/calendar/helpers/CssColors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package org.fossify.calendar.helpers

class CssColors {
private val nameToRgbMap: Map<String, Int> = mapOf(
"aliceblue" to 0xFFF0F8FF.toInt(),
"antiquewhite" to 0xFFFAEBD7.toInt(),
"aqua" to 0xFF00FFFF.toInt(),
"aquamarine" to 0xFF7FFFD4.toInt(),
"azure" to 0xFFF0FFFF.toInt(),
"beige" to 0xFFF5F5DC.toInt(),
"bisque" to 0xFFFFE4C4.toInt(),
"black" to 0xFF000000.toInt(),
"blanchedalmond" to 0xFFFFEBCD.toInt(),
"blue" to 0xFF0000FF.toInt(),
"blueviolet" to 0xFF8A2BE2.toInt(),
"brown" to 0xFFA52A2A.toInt(),
"burlywood" to 0xFFDEB887.toInt(),
"cadetblue" to 0xFF5F9EA0.toInt(),
"chartreuse" to 0xFF7FFF00.toInt(),
"chocolate" to 0xFFD2691E.toInt(),
"coral" to 0xFFFF7F50.toInt(),
"cornflowerblue" to 0xFF6495ED.toInt(),
"cornsilk" to 0xFFFFF8DC.toInt(),
"crimson" to 0xFFDC143C.toInt(),
"cyan" to 0xFF00FFFF.toInt(),
"darkblue" to 0xFF00008B.toInt(),
"darkcyan" to 0xFF008B8B.toInt(),
"darkgoldenrod" to 0xFFB8860B.toInt(),
"darkgray" to 0xFFA9A9A9.toInt(),
"darkgreen" to 0xFF006400.toInt(),
"darkgrey" to 0xFFA9A9A9.toInt(),
"darkkhaki" to 0xFFBDB76B.toInt(),
"darkmagenta" to 0xFF8B008B.toInt(),
"darkolivegreen" to 0xFF556B2F.toInt(),
"darkorange" to 0xFFFF8C00.toInt(),
"darkorchid" to 0xFF9932CC.toInt(),
"darkred" to 0xFF8B0000.toInt(),
"darksalmon" to 0xFFE9967A.toInt(),
"darkseagreen" to 0xFF8FBC8F.toInt(),
"darkslateblue" to 0xFF483D8B.toInt(),
"darkslategray" to 0xFF2F4F4F.toInt(),
"darkslategrey" to 0xFF2F4F4F.toInt(),
"darkturquoise" to 0xFF00CED1.toInt(),
"darkviolet" to 0xFF9400D3.toInt(),
"deeppink" to 0xFFFF1493.toInt(),
"deepskyblue" to 0xFF00BFFF.toInt(),
"dimgray" to 0xFF696969.toInt(),
"dimgrey" to 0xFF696969.toInt(),
"dodgerblue" to 0xFF1E90FF.toInt(),
"firebrick" to 0xFFB22222.toInt(),
"floralwhite" to 0xFFFFFAF0.toInt(),
"forestgreen" to 0xFF228B22.toInt(),
"fuchsia" to 0xFFFF00FF.toInt(),
"gainsboro" to 0xFFDCDCDC.toInt(),
"ghostwhite" to 0xFFF8F8FF.toInt(),
"gold" to 0xFFFFD700.toInt(),
"goldenrod" to 0xFFDAA520.toInt(),
"gray" to 0xFF808080.toInt(),
"green" to 0xFF008000.toInt(),
"greenyellow" to 0xFFADFF2F.toInt(),
"grey" to 0xFF808080.toInt(),
"honeydew" to 0xFFF0FFF0.toInt(),
"hotpink" to 0xFFFF69B4.toInt(),
"indianred" to 0xFFCD5C5C.toInt(),
"indigo" to 0xFF4B0082.toInt(),
"ivory" to 0xFFFFFFF0.toInt(),
"khaki" to 0xFFF0E68C.toInt(),
"lavender" to 0xFFE6E6FA.toInt(),
"lavenderblush" to 0xFFFFF0F5.toInt(),
"lawngreen" to 0xFF7CFC00.toInt(),
"lemonchiffon" to 0xFFFFFACD.toInt(),
"lightblue" to 0xFFADD8E6.toInt(),
"lightcoral" to 0xFFF08080.toInt(),
"lightcyan" to 0xFFE0FFFF.toInt(),
"lightgoldenrodyellow" to 0xFFFAFAD2.toInt(),
"lightgray" to 0xFFD3D3D3.toInt(),
"lightgreen" to 0xFF90EE90.toInt(),
"lightgrey" to 0xFFD3D3D3.toInt(),
"lightpink" to 0xFFFFB6C1.toInt(),
"lightsalmon" to 0xFFFFA07A.toInt(),
"lightseagreen" to 0xFF20B2AA.toInt(),
"lightskyblue" to 0xFF87CEFA.toInt(),
"lightslategray" to 0xFF778899.toInt(),
"lightslategrey" to 0xFF778899.toInt(),
"lightsteelblue" to 0xFFB0C4DE.toInt(),
"lightyellow" to 0xFFFFFFE0.toInt(),
"lime" to 0xFF00FF00.toInt(),
"limegreen" to 0xFF32CD32.toInt(),
"linen" to 0xFFFAF0E6.toInt(),
"magenta" to 0xFFFF00FF.toInt(),
"maroon" to 0xFF800000.toInt(),
"mediumaquamarine" to 0xFF66CDAA.toInt(),
"mediumblue" to 0xFF0000CD.toInt(),
"mediumorchid" to 0xFFBA55D3.toInt(),
"mediumpurple" to 0xFF9370DB.toInt(),
"mediumseagreen" to 0xFF3CB371.toInt(),
"mediumslateblue" to 0xFF7B68EE.toInt(),
"mediumspringgreen" to 0xFF00FA9A.toInt(),
"mediumturquoise" to 0xFF48D1CC.toInt(),
"mediumvioletred" to 0xFFC71585.toInt(),
"midnightblue" to 0xFF191970.toInt(),
"mintcream" to 0xFFF5FFFA.toInt(),
"mistyrose" to 0xFFFFE4E1.toInt(),
"moccasin" to 0xFFFFE4B5.toInt(),
"navajowhite" to 0xFFFFDEAD.toInt(),
"navy" to 0xFF000080.toInt(),
"oldlace" to 0xFFFDF5E6.toInt(),
"olive" to 0xFF808000.toInt(),
"olivedrab" to 0xFF6B8E23.toInt(),
"orange" to 0xFFFFA500.toInt(),
"orangered" to 0xFFFF4500.toInt(),
"orchid" to 0xFFDA70D6.toInt(),
"palegoldenrod" to 0xFFEEE8AA.toInt(),
"palegreen" to 0xFF98FB98.toInt(),
"paleturquoise" to 0xFFAFEEEE.toInt(),
"palevioletred" to 0xFFDB7093.toInt(),
"papayawhip" to 0xFFFFEFD5.toInt(),
"peachpuff" to 0xFFFFDAB9.toInt(),
"peru" to 0xFFCD853F.toInt(),
"pink" to 0xFFFFC0CB.toInt(),
"plum" to 0xFFDDA0DD.toInt(),
"powderblue" to 0xFFB0E0E6.toInt(),
"purple" to 0xFF800080.toInt(),
"rebeccapurple" to 0xFF663399.toInt(),
naveensingh marked this conversation as resolved.
Show resolved Hide resolved
"red" to 0xFFFF0000.toInt(),
"rosybrown" to 0xFFBC8F8F.toInt(),
"royalblue" to 0xFF4169E1.toInt(),
"saddlebrown" to 0xFF8B4513.toInt(),
"salmon" to 0xFFFA8072.toInt(),
"sandybrown" to 0xFFF4A460.toInt(),
"seagreen" to 0xFF2E8B57.toInt(),
"seashell" to 0xFFFFF5EE.toInt(),
"sienna" to 0xFFA0522D.toInt(),
"silver" to 0xFFC0C0C0.toInt(),
"skyblue" to 0xFF87CEEB.toInt(),
"slateblue" to 0xFF6A5ACD.toInt(),
"slategray" to 0xFF708090.toInt(),
"slategrey" to 0xFF708090.toInt(),
"snow" to 0xFFFFFAFA.toInt(),
"springgreen" to 0xFF00FF7F.toInt(),
"steelblue" to 0xFF4682B4.toInt(),
"tan" to 0xFFD2B48C.toInt(),
"teal" to 0xFF008080.toInt(),
"thistle" to 0xFFD8BFD8.toInt(),
"tomato" to 0xFFFF6347.toInt(),
"turquoise" to 0xFF40E0D0.toInt(),
"violet" to 0xFFEE82EE.toInt(),
"wheat" to 0xFFF5DEB3.toInt(),
"white" to 0xFFFFFFFF.toInt(),
"whitesmoke" to 0xFFF5F5F5.toInt(),
"yellow" to 0xFFFFFF00.toInt(),
"yellowgreen" to 0xFF9ACD32.toInt()
)

fun cssNameToRgb(name: String): Int? {
return nameToRgbMap[name]
}

fun findClosestCssColor(color: Int): String? {
val alpha = (color shr 24) and 0xFF
val red = (color shr 16) and 0xFF
val green = (color shr 8) and 0xFF
val blue = color and 0xFF

return nameToRgbMap.minByOrNull { (_, value) ->
val currAlpha = (value shr 24) and 0xFF
val currRed = (value shr 16) and 0xFF
val currGreen = (value shr 8) and 0xFF
val currBlue = value and 0xFF
distance(alpha, red, green, blue, currAlpha, currRed, currGreen, currBlue)
}?.key
}

private fun distance(a1: Int, r1: Int, g1: Int, b1: Int,
a2: Int, r2: Int, g2: Int, b2: Int): Int {
val powA = (a1 - a2) * (a1 - a2)
val powR = (r1 - r2) * (r1 - r2)
val powG = (g1 - g2) * (g1 - g2)
val powB = (b1 - b2) * (b1 - b2)
return powA + powR + powG + powB
}
}
14 changes: 10 additions & 4 deletions app/src/main/kotlin/org/fossify/calendar/helpers/EventsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,7 @@ class EventsHelper(val context: Context) {
.filterNot { it.repetitionExceptions.contains(Formatter.getDayCodeFromTS(it.startTS)) }
.toMutableList() as ArrayList<Event>

val eventTypeColors = LongSparseArray<Int>()
context.eventTypesDB.getEventTypes().forEach {
eventTypeColors.put(it.id!!, it.color)
}
val eventTypeColors = getEventTypeColors()

events.forEach {
if (it.isTask()) {
Expand Down Expand Up @@ -649,4 +646,13 @@ class EventsHelper(val context: Context) {
events = events.distinctBy { it.id } as ArrayList<Event>
return events
}

fun getEventTypeColors(): LongSparseArray<Int> {
val eventTypeColors = LongSparseArray<Int>()
context.eventTypesDB.getEventTypes().forEach {
eventTypeColors.put(it.id!!, it.color)
}

return eventTypeColors
}
}
18 changes: 18 additions & 0 deletions app/src/main/kotlin/org/fossify/calendar/helpers/IcsExporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.provider.CalendarContract.Events
import org.fossify.calendar.R
import org.fossify.calendar.extensions.calDAVHelper
import org.fossify.calendar.extensions.eventTypesDB
import org.fossify.calendar.extensions.eventsHelper
import org.fossify.calendar.helpers.IcsExporter.ExportResult.EXPORT_FAIL
import org.fossify.calendar.helpers.IcsExporter.ExportResult.EXPORT_OK
import org.fossify.calendar.helpers.IcsExporter.ExportResult.EXPORT_PARTIAL
Expand All @@ -29,6 +30,7 @@ class IcsExporter(private val context: Context) {
private val reminderLabel = context.getString(R.string.reminder)
private val exportTime = Formatter.getExportedTime(System.currentTimeMillis())
private val status = 1
private val cssColors = CssColors()

fun exportEvents(
outputStream: OutputStream?,
Expand Down Expand Up @@ -130,11 +132,19 @@ class IcsExporter(private val context: Context) {
}

private fun writeEvent(writer: BufferedWriter, event: Event) {
val eventTypeColors = context.eventsHelper.getEventTypeColors()
with(writer) {
writeLn(BEGIN_EVENT)
event.title.replace("\n", "\\n").let { if (it.isNotEmpty()) writeLn("$SUMMARY:$it") }
event.importId.let { if (it.isNotEmpty()) writeLn("$UID$it") }
writeLn("$CATEGORY_COLOR${context.eventTypesDB.getEventTypeWithId(event.eventType)?.color}")
if (event.color != 0 && event.color != eventTypeColors[event.eventType]) {
val color = cssColors.findClosestCssColor(event.color)
if (color != null) {
writeLn("$COLOR${color}")
}
writeLn("$FOSSIFY_COLOR${event.color}")
}
writeLn("$CATEGORIES${context.eventTypesDB.getEventTypeWithId(event.eventType)?.title}")
writeLn("$LAST_MODIFIED:${Formatter.getExportedTime(event.lastUpdated)}")
writeLn("$TRANSP${if (event.availability == Events.AVAILABILITY_FREE) TRANSPARENT else OPAQUE}")
Expand Down Expand Up @@ -163,11 +173,19 @@ class IcsExporter(private val context: Context) {
}

private fun writeTask(writer: BufferedWriter, task: Event) {
val eventTypeColors = context.eventsHelper.getEventTypeColors()
with(writer) {
writeLn(BEGIN_TASK)
task.title.replace("\n", "\\n").let { if (it.isNotEmpty()) writeLn("$SUMMARY:$it") }
task.importId.let { if (it.isNotEmpty()) writeLn("$UID$it") }
writeLn("$CATEGORY_COLOR${context.eventTypesDB.getEventTypeWithId(task.eventType)?.color}")
if (task.color != 0 && task.color != eventTypeColors[task.eventType]) {
val color = cssColors.findClosestCssColor(task.color)
if (color != null) {
writeLn("$COLOR${color}")
}
writeLn("$FOSSIFY_COLOR${task.color}")
}
writeLn("$CATEGORIES${context.eventTypesDB.getEventTypeWithId(task.eventType)?.title}")
writeLn("$LAST_MODIFIED:${Formatter.getExportedTime(task.lastUpdated)}")
task.location.let { if (it.isNotEmpty()) writeLn("$LOCATION:$it") }
Expand Down
17 changes: 16 additions & 1 deletion app/src/main/kotlin/org/fossify/calendar/helpers/IcsImporter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ class IcsImporter(val activity: SimpleActivity) {
private var isParsingTask = false
private var curReminderTriggerMinutes = REMINDER_OFF
private var curReminderTriggerAction = REMINDER_NOTIFICATION
private var curColor = 0
private val eventsHelper = activity.eventsHelper
private val cssColors = CssColors()

private var eventsImported = 0
private var eventsFailed = 0
Expand Down Expand Up @@ -166,6 +168,17 @@ class IcsImporter(val activity: SimpleActivity) {
if (color.trimStart('-').areDigitsOnly()) {
curCategoryColor = Integer.parseInt(color)
}
} else if (line.startsWith(COLOR)) {
val colorName = line.substring(COLOR.length)
val color = cssColors.cssNameToRgb(colorName)
if (color != null) {
curColor = color
}
} else if (line.startsWith(FOSSIFY_COLOR)) {
val color = line.substring(FOSSIFY_COLOR.length)
if (color.trimStart('-').areDigitsOnly()) {
curColor = Integer.parseInt(color)
}
} else if (line.startsWith(MISSING_YEAR)) {
if (line.substring(MISSING_YEAR.length) == "1") {
curFlags = curFlags or FLAG_MISSING_YEAR
Expand Down Expand Up @@ -284,7 +297,8 @@ class IcsImporter(val activity: SimpleActivity) {
source,
curAvailability,
type = curType,
status = curStatus
status = curStatus,
color = curColor
)

if (isAllDay && curEnd > curStart && !event.isTask()) {
Expand Down Expand Up @@ -466,5 +480,6 @@ class IcsImporter(val activity: SimpleActivity) {
curReminderTriggerMinutes = REMINDER_OFF
curReminderTriggerAction = REMINDER_NOTIFICATION
curType = TYPE_EVENT
curColor = 0
}
}
Loading