From 1efd8087838e27ac4add7ad614de8eb94fb0c18f Mon Sep 17 00:00:00 2001 From: Matt Mascarenhas Date: Wed, 4 Apr 2018 23:39:38 +0100 Subject: [PATCH] cinera.c: Distinguish speakers from chat comments It treats co-hosts and guests differently from chat commenters, styling and categorising annotations for them such that their contributions don't come under the "Chat comment" medium Also do some essentially cosmetic code compression of the marker cases and other things cinera_player_pre.js: Make the credits menu initially focus the host's person if they have no support, rather than the first credited person who has support --- cinera/cinera.c | 418 ++++++++++++++++++++++++------------ cinera/cinera_player_pre.js | 2 +- 2 files changed, 282 insertions(+), 138 deletions(-) diff --git a/cinera/cinera.c b/cinera/cinera.c index 7ceaee9..3d63bef 100644 --- a/cinera/cinera.c +++ b/cinera/cinera.c @@ -14,7 +14,7 @@ typedef struct version CINERA_APP_VERSION = { .Major = 0, .Minor = 5, - .Patch = 41 + .Patch = 42 }; // TODO(matt): Copy in the DB 3 stuff from cinera_working.c @@ -716,6 +716,18 @@ StringsDiffer(char *A, char *B) // NOTE(matt): Two null-terminated strings return *A - *B; } +int +StringsDifferCaseInsensitive(char *A, char *B) // NOTE(matt): Two null-terminated strings +{ + while(*A && *B && + ((*A >= 'A' && *A <= 'Z') ? *A + ('a' - 'A') : *A) == + ((*B >= 'A' && *B <= 'Z') ? *B + ('a' - 'A') : *B)) + { + ++A, ++B; + } + return *A - *B; +} + bool StringsDifferT(char *A, // NOTE(matt): Null-terminated string char *B, // NOTE(matt): Not null-terminated string (e.g. one mid-buffer) @@ -1146,7 +1158,7 @@ CharToColour(char Char) return Colour; } -hsl_colour * +void StringToColourHash(hsl_colour *Colour, char *String) { Colour->Hue = 0; @@ -1162,7 +1174,6 @@ StringToColourHash(hsl_colour *Colour, char *String) Colour->Hue = Colour->Hue % 360; Colour->Saturation = Colour->Saturation % 26 + 74; - return(Colour); } char * @@ -1242,6 +1253,20 @@ ConstructURLPrefix(buffer *URLPrefix, int IncludeType, int PageType) } } +typedef struct +{ + char Abbreviation[32]; + hsl_colour Colour; + credential_info *Credential; + bool Seen; +} speaker; + +typedef struct +{ + speaker Speaker[16]; + int Count; +} speakers; + enum { CreditsError_NoHost, @@ -1250,13 +1275,18 @@ enum } credits_errors; int -SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role) +SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char *Role, speakers *Speakers) { bool Found = FALSE; for(int CredentialIndex = 0; CredentialIndex < ArrayCount(Credentials); ++CredentialIndex) { if(!StringsDiffer(Person, Credentials[CredentialIndex].Username)) { + if(Speakers) + { + Speakers->Speaker[Speakers->Count].Credential = &Credentials[CredentialIndex]; + ++Speakers->Count; + } Found = TRUE; if(*HasCreditsMenu == FALSE) { @@ -1310,41 +1340,159 @@ SearchCredentials(buffer *CreditsMenu, bool *HasCreditsMenu, char *Person, char " \n"); } } - return Found ? 0 : CreditsError_NoCredentials; + return Found ? RC_SUCCESS : CreditsError_NoCredentials; +} + +void +ClearString(char *String) +{ + while(*String) + { + *String++ = '\0'; + } +} + +void +InitialString(char *Dest, char *Src) +{ + ClearString(Dest); + *Dest++ = *Src++; + while(*Src++) + { + if(*Src == ' ') + { + ++Src; + if(*Src) + { + *Dest++ = *Src; + } + } + } +} + +void +GetFirstSubstring(char *Dest, char *Src) +{ + ClearString(Dest); + while(*Src && *Src != ' ') + { + *Dest++ = *Src++; + } +} + +void +InitialAndGetFinalString(char *Dest, char *Src) +{ + ClearString(Dest); + int SrcLength = StringLength(Src); + char *SrcPtr = Src + SrcLength - 1; + while(SrcPtr > Src && *SrcPtr != ' ') + { + --SrcPtr; + } + if(*SrcPtr == ' ' && SrcPtr - Src < SrcLength - 1) + { + ++SrcPtr; + } + + if(Src < SrcPtr) + { + *Dest++ = *Src++; + *Dest++ = '.'; + *Dest++ = ' '; + + while(Src < SrcPtr - 1) + { + if(*Src == ' ') + { + ++Src; + if(*Src) + { + *Dest++ = *Src; + *Dest++ = '.'; + *Dest++ = ' '; + } + } + ++Src; + } + } + + CopyString(Dest, SrcPtr); +} + +bool +AbbreviationsClash(speakers *Speakers) +{ + for(int i = 0; i < Speakers->Count; ++i) + { + for(int j = i + 1; j < Speakers->Count; ++j) + { + if(!StringsDiffer(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[j].Abbreviation)) + { + return TRUE; + } + } + } + return FALSE; +} + +void +SortAndAbbreviateSpeakers(speakers *Speakers) +{ + for(int i = 0; i < Speakers->Count; ++i) + { + for(int j = i + 1; j < Speakers->Count; ++j) + { + if(StringsDiffer(Speakers->Speaker[i].Credential->Username, Speakers->Speaker[j].Credential->Username) > 0) + { + credential_info *Temp = Speakers->Speaker[j].Credential; + Speakers->Speaker[j].Credential = Speakers->Speaker[i].Credential; + Speakers->Speaker[i].Credential = Temp; + break; + } + } + } + + for(int i = 0; i < Speakers->Count; ++i) + { + StringToColourHash(&Speakers->Speaker[i].Colour, Speakers->Speaker[i].Credential->Username); + InitialString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); + } + + int Attempt = 0; + while(AbbreviationsClash(Speakers)) + { + for(int i = 0; i < Speakers->Count; ++i) + { + switch(Attempt) + { + case 0: GetFirstSubstring(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); break; + case 1: InitialAndGetFinalString(Speakers->Speaker[i].Abbreviation, Speakers->Speaker[i].Credential->CreditedName); break; + case 2: ClearCopyStringNoFormat(Speakers->Speaker[i].Abbreviation, sizeof(Speakers->Speaker[i].Abbreviation), Speakers->Speaker[i].Credential->Username); break; + } + } + ++Attempt; + } } int -BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata) +BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Metadata, speakers *Speakers) // TODO(matt): Make this take the Credentials, once we are parsing them from a config { - if(Metadata->member) + if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host", Speakers) == CreditsError_NoCredentials) { - if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->member, "Host")) - { - printf("No credentials for member %s. Please contact matt@handmadedev.org with their:\n" - " Full name\n" - " Homepage URL (optional)\n" - " Financial support info, e.g. Patreon URL (optional)\n", Metadata->member); - return CreditsError_NoCredentials; - } - } - else - { - if(*HasCreditsMenu == TRUE) - { - CopyStringToBuffer(CreditsMenu, - " \n" - " \n"); - } - fprintf(stderr, "Missing \"member\" in the [video] node\n"); - return CreditsError_NoHost; + printf("No credentials for member %s. Please contact matt@handmadedev.org with their:\n" + " Full name\n" + " Homepage URL (optional)\n" + " Financial support info, e.g. Patreon URL (optional)\n", Metadata->member); + return CreditsError_NoCredentials; } if(Metadata->co_host_count > 0) { for(int i = 0; i < Metadata->co_host_count; ++i) { - if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host")) + if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->co_hosts[i], "Co-host", Speakers) == CreditsError_NoCredentials) { printf("No credentials for co-host %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" @@ -1359,7 +1507,7 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta { for(int i = 0; i < Metadata->guest_count; ++i) { - if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest")) + if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->guests[i], "Guest", Speakers) == CreditsError_NoCredentials) { printf("No credentials for guest %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" @@ -1370,11 +1518,16 @@ BuildCredits(buffer *CreditsMenu, bool *HasCreditsMenu, HMML_VideoMetaData *Meta } } + if(Speakers->Count > 1) + { + SortAndAbbreviateSpeakers(Speakers); + } + if(Metadata->annotator_count > 0) { for(int i = 0; i < Metadata->annotator_count; ++i) { - if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator")) + if(SearchCredentials(CreditsMenu, HasCreditsMenu, Metadata->annotators[i], "Annotator", 0) == CreditsError_NoCredentials) { printf("No credentials for annotator %s. Please contact matt@handmadedev.org with their:\n" " Full name\n" @@ -2713,6 +2866,32 @@ typedef struct short int PrevIndex, ThisIndex, NextIndex; } neighbourhood; +int +LinearSearchForSpeaker(speakers Speakers, char *Username) +{ + for(int i = 0; i < Speakers.Count; ++i) + { + if(!StringsDifferCaseInsensitive(Speakers.Speaker[i].Credential->Username, Username)) + { + return i; + } + } + return -1; +} + +bool +IsCategorisedAFK(HMML_Annotation Anno) +{ + for(int i = 0; i < Anno.marker_count; ++i) + { + if(!StringsDiffer(Anno.markers[i].marker, "afk")) + { + return TRUE; + } + } + return FALSE; +} + int HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filename, neighbourhood *N) { @@ -3019,7 +3198,8 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen CopyStringToBuffer(&CollationBuffers->Player, "
\n"); - switch(BuildCredits(&CreditsMenu, &HasCreditsMenu, &HMML.metadata)) + speakers Speakers = { 0 }; + switch(BuildCredits(&CreditsMenu, &HasCreditsMenu, &HMML.metadata, &Speakers)) { case CreditsError_NoHost: case CreditsError_NoAnnotator: @@ -3114,35 +3294,45 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen CopyStringToBuffer(&AnnotationClass, " class=\"marker"); - if(Anno->author) + if((Anno->author || Speakers.Count > 1) && !IsCategorisedAFK(*Anno)) { - if(!HasFilterMenu) - { - HasFilterMenu = TRUE; - } - InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "authored"); - hsl_colour AuthorColour; - StringToColourHash(&AuthorColour, Anno->author); - if(Config.Edition == EDITION_NETWORK) - { - fprintf(stderr, "%s:%d - TODO(matt): Implement author hoverbox\n", __FILE__, __LINE__); - // NOTE(matt): We should get instructions on how to get this info in the config - CopyStringToBuffer(&Text, - "%s ", - Anno->author, - AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness, - AuthorColour.Hue, AuthorColour.Saturation, - Anno->author); - } - else + int SpeakerIndex; + if(Anno->author && (SpeakerIndex = LinearSearchForSpeaker(Speakers, Anno->author)) == -1) { + if(!HasFilterMenu) + { + HasFilterMenu = TRUE; + } + InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, "authored"); + hsl_colour AuthorColour; + StringToColourHash(&AuthorColour, Anno->author); + // TODO(matt): That EDITION_NETWORK site database API-polling stuff CopyStringToBuffer(&Text, "%s ", AuthorColour.Hue, AuthorColour.Saturation, AuthorColour.Lightness, AuthorColour.Hue, AuthorColour.Saturation, Anno->author); } + else + { + if(!Anno->author) + { + SpeakerIndex = LinearSearchForSpeaker(Speakers, HMML.metadata.member); + } + CopyStringToBuffer(&Text, + "%s: ", + Speakers.Speaker[SpeakerIndex].Colour.Hue, + Speakers.Speaker[SpeakerIndex].Colour.Saturation, + Speakers.Speaker[SpeakerIndex].Colour.Lightness, + + Speakers.Speaker[SpeakerIndex].Colour.Hue, + Speakers.Speaker[SpeakerIndex].Colour.Saturation, + + Speakers.Speaker[SpeakerIndex].Seen == FALSE ? Speakers.Speaker[SpeakerIndex].Credential->CreditedName : Speakers.Speaker[SpeakerIndex].Abbreviation); + + Speakers.Speaker[SpeakerIndex].Seen = TRUE; + } } char *InPtr = Anno->text; @@ -3153,85 +3343,48 @@ HMMLToBuffers(buffers *CollationBuffers, template **BespokeTemplate, char *Filen if(MarkerIndex < Anno->marker_count && InPtr - Anno->text == Anno->markers[MarkerIndex].offset) { - // TODO(matt): Consider switching on the Anno->markers[MarkerIndex].type and 100% ensuring this is all correct - // I wonder if HMML_CATEGORY should do InPtr += StringLength(Readable); like the others, and also whether HMML_MEMBER and HMML_PROJECT could be - // identical, except only for their class ("member" and "project" respectively) - // Pretty goddamn sure we can totally compress these cases, but let's do it tomorrow when we're fresh char *Readable = Anno->markers[MarkerIndex].parameter ? Anno->markers[MarkerIndex].parameter : Anno->markers[MarkerIndex].marker; - if(Anno->markers[MarkerIndex].type == HMML_MEMBER) + switch(Anno->markers[MarkerIndex].type) { - hsl_colour MemberColour; - StringToColourHash(&MemberColour, Anno->markers[MarkerIndex].marker); - if(Config.Edition == EDITION_NETWORK) - { - fprintf(stderr, "%s:%d - TODO(matt): Implement member hoverbox\n", __FILE__, __LINE__); - // NOTE(matt): We should get instructions on how to get this info in the config - CopyStringToBuffer(&Text, - "%.*s", - Anno->markers[MarkerIndex].marker, - MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness, - MemberColour.Hue, MemberColour.Saturation, - StringLength(Readable), InPtr); - } - else - { - CopyStringToBuffer(&Text, - "%.*s", - MemberColour.Hue, MemberColour.Saturation, MemberColour.Lightness, - MemberColour.Hue, MemberColour.Saturation, - StringLength(Readable), InPtr); - } + case HMML_MEMBER: + case HMML_PROJECT: + { + // TODO(matt): That EDITION_NETWORK site database API-polling stuff + hsl_colour Colour; + StringToColourHash(&Colour, Anno->markers[MarkerIndex].marker); + CopyStringToBuffer(&Text, + "%s", + Anno->markers[MarkerIndex].type == HMML_MEMBER ? "member" : "project", + Colour.Hue, Colour.Saturation, Colour.Lightness, + Colour.Hue, Colour.Saturation, + Readable); - InPtr += StringLength(Readable); - ++MarkerIndex; - } - else if(Anno->markers[MarkerIndex].type == HMML_PROJECT) - { - hsl_colour ProjectColour; - StringToColourHash(&ProjectColour, Anno->markers[MarkerIndex].marker); - if(Config.Edition == EDITION_NETWORK) - { - fprintf(stderr, "%s:%d - TODO(matt): Implement project hoverbox\n", __FILE__, __LINE__); - // NOTE(matt): We should get instructions on how to get this info in the config - CopyStringToBuffer(&Text, - "%s", - Anno->markers[MarkerIndex].marker, - ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness, - ProjectColour.Hue, ProjectColour.Saturation, - Readable); - } - else - { - CopyStringToBuffer(&Text, - "%s", - ProjectColour.Hue, ProjectColour.Saturation, ProjectColour.Lightness, - ProjectColour.Hue, ProjectColour.Saturation, - Readable); - } - InPtr += StringLength(Readable); - ++MarkerIndex; - } - else if(Anno->markers[MarkerIndex].type == HMML_CATEGORY) - { - switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker)) - { - case RC_SUCCESS: - case RC_NOOP: - break; - case RC_ERROR_FILE: - case RC_ERROR_MEMORY: - hmml_free(&HMML); - return RC_ERROR_FATAL; - }; - if(!HasFilterMenu) - { - HasFilterMenu = TRUE; - } - InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker); - ++MarkerIndex; + } break; + case HMML_CATEGORY: + { + switch(GenerateTopicColours(Anno->markers[MarkerIndex].marker)) + { + case RC_SUCCESS: + case RC_NOOP: + break; + case RC_ERROR_FILE: + case RC_ERROR_MEMORY: + hmml_free(&HMML); + return RC_ERROR_FATAL; + }; + if(!HasFilterMenu) + { + HasFilterMenu = TRUE; + } + InsertCategory(&Topics, &LocalTopics, &Media, &LocalMedia, Anno->markers[MarkerIndex].marker); + CopyStringToBuffer(&Text, Readable); + } break; + case HMML_MARKER_COUNT: break; } + InPtr += StringLength(Readable); + ++MarkerIndex; } while(RefIndex < Anno->reference_count && @@ -3356,14 +3509,9 @@ AppendedIdentifier: } } - if(Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset) - { - CopyStringToBuffer(&Text, ",%d", RefIdentifier); - } - else - { - CopyStringToBuffer(&Text, "%d", RefIdentifier); - } + CopyStringToBuffer(&Text, "%s%d", + Anno->references[RefIndex].offset == Anno->references[RefIndex-1].offset ? "," : "", + RefIdentifier); ++RefIndex; ++RefIdentifier; @@ -3375,29 +3523,25 @@ AppendedIdentifier: { case '<': CopyStringToBuffer(&Text, "<"); - InPtr++; break; case '>': CopyStringToBuffer(&Text, ">"); - InPtr++; break; case '&': CopyStringToBuffer(&Text, "&"); - InPtr++; break; case '\"': CopyStringToBuffer(&Text, """); - InPtr++; break; case '\'': CopyStringToBuffer(&Text, "'"); - InPtr++; break; default: - *Text.Ptr++ = *InPtr++; + *Text.Ptr++ = *InPtr; *Text.Ptr = '\0'; break; } + ++InPtr; } } diff --git a/cinera/cinera_player_pre.js b/cinera/cinera_player_pre.js index e61b7ce..90740c0 100644 --- a/cinera/cinera_player_pre.js +++ b/cinera/cinera_player_pre.js @@ -388,7 +388,7 @@ function toggleMenuVisibility(element) { { if(!lastFocusedCreditItem) { - if(element.querySelectorAll(".credit .support")[0]) + if(element.querySelectorAll(".credit .person")[0].nextElementSibling) { lastFocusedCreditItem = element.querySelectorAll(".credit .support")[0]; focusedElement = lastFocusedCreditItem;