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;