diff --git a/.github/workflows/integration-testing.yaml b/.github/workflows/integration-testing.yaml index fb9b9cf2eb..887dd2f73f 100644 --- a/.github/workflows/integration-testing.yaml +++ b/.github/workflows/integration-testing.yaml @@ -8,7 +8,7 @@ concurrency: integration_environment jobs: variables: - if: ${{ github.event_name == 'merge_group' }} + if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest outputs: date: ${{ steps.data.outputs.date }} @@ -29,7 +29,7 @@ jobs: echo "current_branch=merge-queue" >> $GITHUB_OUTPUT build-generic: - if: ${{ github.event_name == 'merge_group' }} + if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }} name: "Integration Image Build" needs: - variables @@ -83,7 +83,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} build-derived: - if: ${{ github.event_name == 'merge_group' }} + if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest name: "Integration Image Build Stage 2" permissions: @@ -140,7 +140,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} run-tests: - if: ${{ github.event_name == 'merge_group' }} + if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }} name: "Playwright" # This name is referenced when slacking status needs: - build-derived @@ -204,7 +204,7 @@ jobs: - name: Uninstall run: helm delete integration-${{ needs.variables.outputs.commit }} -n ${{ secrets.DEV_SANDBOX_NAMESPACE }} --debug --timeout 10m0s ending-notification: - if: ${{ github.event_name == 'merge_group' }} + if: ${{ github.event_name == 'merge_group' || github.event_name == 'workflow_dispatch' }} runs-on: ubuntu-latest needs: - run-tests diff --git a/e2e-tests/globals.ts b/e2e-tests/globals.ts index a4b53698f2..959dd8b4a6 100644 --- a/e2e-tests/globals.ts +++ b/e2e-tests/globals.ts @@ -5,6 +5,12 @@ export const LANGUAGES = { 'HE': 'hebrew', } +export const SOURCE_LANGUAGES = { + 'EN': /^(תרגום|Translation)$/, + 'HE': /^(מקור|Source)$/, + 'BI': /^(מקור ותרגום|Source with Translation)$/ +} + export const cookieObject = { "name": "interfaceLang", "value": DEFAULT_LANGUAGE, diff --git a/e2e-tests/tests/interface-language-is-sticky.spec.ts b/e2e-tests/tests/interface-language-is-sticky.spec.ts new file mode 100644 index 0000000000..682d48ee93 --- /dev/null +++ b/e2e-tests/tests/interface-language-is-sticky.spec.ts @@ -0,0 +1,57 @@ +import {test, expect} from '@playwright/test'; +import {changeLanguageOfText, goToPageWithLang, isIsraelIp} from "../utils"; +import {LANGUAGES, SOURCE_LANGUAGES} from '../globals' + +const interfaceTextHE = 'מקורות'; +const interfaceTextEN = 'Texts'; + +[ + // Hebrew Interface and English Source + {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE, + sourceLanguage: 'English', sourceLanguageToggle: SOURCE_LANGUAGES.EN, + expectedSourceText: 'When God began to create', expectedBilingualText: '', expectedInterfaceText: interfaceTextHE }, + + // Hebrew Interface and Bilingual Source + {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE, + sourceLanguage: 'Bilingual', sourceLanguageToggle: SOURCE_LANGUAGES.BI, + expectedSourceText: 'רֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃', expectedBilingualText: 'When God began to create', expectedInterfaceText: interfaceTextHE }, + + // Hebrew Interface and Hebrew Source + {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE, + sourceLanguage: 'Hebrew', sourceLanguageToggle: SOURCE_LANGUAGES.HE, + expectedSourceText: 'רֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃', expectedBilingualText: '', expectedInterfaceText: interfaceTextHE }, + + // English Interface and English Source + {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN, + sourceLanguage: 'English', sourceLanguageToggle: SOURCE_LANGUAGES.EN, + expectedSourceText: 'When God began to create', expectedBilingualText: '', expectedInterfaceText: interfaceTextEN }, + + // English Interface and Bilingual Source + {interfaceLanguage: 'English', sinterfaceLanguageToggle: LANGUAGES.EN, + sourceLanguage: 'Bilingual', sourceLanguageToggle: SOURCE_LANGUAGES.BI, + expectedSourceText: 'רֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃', expectedBilingualText: 'When God began to create', + expectedInterfaceText: interfaceTextEN }, + + // English Interface and Hebrew Source + {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN, + sourceLanguage: 'Hebrew', sourceLanguageToggle: SOURCE_LANGUAGES.HE, + expectedSourceText: 'רֵאשִׁ֖ית בָּרָ֣א אֱלֹהִ֑ים אֵ֥ת הַשָּׁמַ֖יִם וְאֵ֥ת הָאָֽרֶץ׃', expectedBilingualText: '', expectedInterfaceText: interfaceTextEN } + +].forEach(({interfaceLanguage, interfaceLanguageToggle, sourceLanguage, sourceLanguageToggle, expectedSourceText, expectedBilingualText, expectedInterfaceText}) => { + test(`${interfaceLanguage} Interface Language with ${sourceLanguage} Source`, async ({ context }) => { + + // Navigating to Bereshit with selected Interface Language, Hebrew or English + const page = await goToPageWithLang(context,'/Genesis.1',`${interfaceLanguageToggle}`) + + // Selecting Source Language + await changeLanguageOfText(page, sourceLanguageToggle) + + // Locating the source text segment, then verifying translation + await expect(page.locator('div.segmentNumber').first().locator('..').locator('p')).toContainText(`${expectedSourceText}`) + + // Validate Hebrew interface language is still toggled + const textLink = page.locator('a.textLink').first() + await expect(textLink).toHaveText(`${expectedInterfaceText}`) + + }) +}) \ No newline at end of file diff --git a/e2e-tests/tests/reader.spec.ts b/e2e-tests/tests/reader.spec.ts index 4938ec0f2a..8236e263ac 100644 --- a/e2e-tests/tests/reader.spec.ts +++ b/e2e-tests/tests/reader.spec.ts @@ -15,14 +15,14 @@ test('Navigate to bereshit', async ({ context }) => { }); test('Verify translations', async ({ context }) => { - const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he'); + const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=hebrew|Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he'); await page.getByRole('link', { name: 'Translations (4)' }).click(); await page.locator('#panel-1').getByText('Loading...').waitFor({ state: 'detached' }); page.getByText('A. Cohen, Cambridge University Press, 1921', { exact: true }) }); test('Get word description', async ({ context }) => { - const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he'); + const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=hebrew|Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he'); await page.getByRole('link', { name: 'ר\' נחוניא בן הקנה' }).click(); await page.locator('#panel-1').getByText('Loading...').waitFor({ state: 'detached' }); await page.getByText('Looking up words...').waitFor({ state: 'detached' }); @@ -31,11 +31,11 @@ test('Get word description', async ({ context }) => { test('Open panel window', async ({ context }) => { - const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=Wikisource_Talmud_Bavli&lang=bi&with=all&lang2=he'); + const page = await goToPageWithLang(context, '/Berakhot.28b.4?vhe=hebrew|Wikisource_Talmud_Bavli&&lang=bi&with=all&lang2=he'); await page.getByText('ולית הלכתא לא כרב הונא ולא כריב"ל כרב הונא הא דאמרן כריב"ל דאריב"ל כיון שהגיע זמ').click(); await page.locator('#panel-1').getByText('Loading...').waitFor({ state: 'detached' }); await page.getByRole('link', { name: 'תלמוד (1)' }).click(); - await page.getByRole('link', { name: 'שבת (1) מלאכות האסורות בשבת ודינים הקשורים לקדושת היום.' }).click(); + await page.getByRole('link', { name: /^שבת/ }).click(); await page.getByText('טעינה...').waitFor({ state: 'detached' }); await page.getByRole('link', { name: 'Open' }).click(); await page.getByRole('heading', { name: 'Loading...' }).getByText('Loading...').waitFor({ state: 'detached' }); diff --git a/e2e-tests/tests/translation-version-name-appears-in-title.spec.ts b/e2e-tests/tests/translation-version-name-appears-in-title.spec.ts new file mode 100644 index 0000000000..46bb51ce58 --- /dev/null +++ b/e2e-tests/tests/translation-version-name-appears-in-title.spec.ts @@ -0,0 +1,80 @@ +import {test, expect} from '@playwright/test'; +import {goToPageWithLang, changeLanguageOfText} from "../utils"; +import {LANGUAGES, SOURCE_LANGUAGES} from '../globals' + +[ + // Hebrew Interface and Hebrew Source + {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE, + sourceLanguage: 'Hebrew', sourceLanguageToggle: SOURCE_LANGUAGES.HE, + translations: 'תרגומים', select: 'בחירה', currentlySelected: 'נוכחי'}, + + // Hebrew Interface and Bilingual Source + {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE, + sourceLanguage: 'Bilingual', sourceLanguageToggle: SOURCE_LANGUAGES.BI, + translations: 'תרגומים', select: 'בחירה', currentlySelected: 'נוכחי'}, + + // Hebrew Interface and English Source + {interfaceLanguage: 'Hebrew', interfaceLanguageToggle: LANGUAGES.HE, + sourceLanguage: 'English', sourceLanguageToggle: SOURCE_LANGUAGES.EN, + translations: 'תרגומים', select: 'בחירה', currentlySelected: 'נוכחי'}, + + // English Interface and English Source + {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN, + sourceLanguage: 'English', sourceLanguageToggle: SOURCE_LANGUAGES.EN, + translations: 'Translations', select: 'Select', currentlySelected: 'Currently Selected'}, + + // English Interface and English Source + {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN, + sourceLanguage: 'Bilingual', sourceLanguageToggle: SOURCE_LANGUAGES.BI, + translations: 'Translations', select: 'Select', currentlySelected: 'Currently Selected'}, + + // English Interface and Hebrew Source + {interfaceLanguage: 'English', interfaceLanguageToggle: LANGUAGES.EN, + sourceLanguage: 'Hebrew', sourceLanguageToggle: SOURCE_LANGUAGES.HE, + translations: 'Translations', select: 'Select', currentlySelected: 'Currently Selected'} + +].forEach(({interfaceLanguage, interfaceLanguageToggle, sourceLanguage, sourceLanguageToggle, translations, currentlySelected, select}) => { + test(`${interfaceLanguage} - translation name appears in title for ${sourceLanguage} source text`, async ({ context }) => { + // Navigate to Bereshit in specified Interface Language + const page = await goToPageWithLang(context,'/Genesis.1', `${interfaceLanguageToggle}`) + + // Change the Source Language of the text + await changeLanguageOfText(page, sourceLanguageToggle) + + // Retain the translation name locator + const translationNameInTitle = page.locator('span.readerTextVersion') + + // Navigate to the Translations sidebar by clicking on the text title + //Clicks on בראשית א׳ / Genesis I + await page.locator('h1').click() + + // Click on Translations + await page.getByRole('link', {name: `${translations}`}).click() + + // Wait for Translations side-bar to load by waiting for 'Translations' header + await page.waitForSelector('h3') + + // Check if the default translation in the title matches the selected translation + // NOTE: We are skipping checking for the default translation here, due to the Hebrew text being default Masoretic + if(sourceLanguage !== 'Hebrew'){ + const defaultTranslation = await translationNameInTitle.textContent() + await expect(page.locator('div.version-with-preview-title-line', {hasText: defaultTranslation!}).getByRole('link')).toHaveText(`${currentlySelected}`) + } + + // TODO: 4th translation, handling Hebrew Interface translations in Hebrew. For example: 'חומש רש״י, רבי שרגא זילברשטיין' should appear in the translation title as written. + const translationNames = ['The Schocken Bible, Everett Fox, 1995 ©', '«Да» project'] + + // Utilizing the traditional for-loop as there are async issues with foreach + for(let i = 0; i < translationNames.length; i++){ + + // "Select" another translation. + await page.locator('div.version-with-preview-title-line', {hasText: translationNames[i]}).getByText(`${select}`).click() + + // Validate selected translation is reflected in title + await expect(translationNameInTitle).toHaveText(translationNames[i]) + + // Validate selected translation says 'Currently Selected' + await expect(page.locator('div.version-with-preview-title-line', {hasText: translationNames[i]}).getByRole('link')).toHaveText(`${currentlySelected}`) + } + }) +}); \ No newline at end of file diff --git a/e2e-tests/utils.ts b/e2e-tests/utils.ts index dc3922d64e..22d75a0270 100644 --- a/e2e-tests/utils.ts +++ b/e2e-tests/utils.ts @@ -1,4 +1,4 @@ -import {DEFAULT_LANGUAGE, LANGUAGES, testUser} from './globals' +import {DEFAULT_LANGUAGE, LANGUAGES, SOURCE_LANGUAGES, testUser} from './globals' import {BrowserContext} from 'playwright-core'; import type { Page } from 'playwright-core'; @@ -23,13 +23,16 @@ export const changeLanguage = async (page: Page, language: string) => { } export const goToPageWithLang = async (context: BrowserContext, url: string, language=DEFAULT_LANGUAGE) => { - if (!langCookies.length) { - const page: Page = await context.newPage(); - await page.goto(''); - await changeLanguage(page, language); - langCookies = await context.cookies(); - } + // If a cookie already has contents, clear it so that the language cookie can be reset + if (langCookies.length) { + await context.clearCookies() + } + const page: Page = await context.newPage(); + await page.goto(''); + await changeLanguage(page, language); + langCookies = await context.cookies(); await context.addCookies(langCookies); + // this is a hack to get the cookie to work const newPage: Page = await context.newPage(); await newPage.goto(url); @@ -65,4 +68,26 @@ export const goToPageWithUser = async (context: BrowserContext, url: string, use export const getPathAndParams = (url: string) => { const urlObj = new URL(url); return urlObj.pathname + urlObj.search; +} + +export const changeLanguageOfText = async (page: Page, sourceLanguage: RegExp) => { + // Clicking on the Source Language toggle + await page.getByAltText('Toggle Reader Menu Display Settings').click() + + // Selecting Source Language + await page.locator('div').filter({ hasText: sourceLanguage }).click() +} + +export const getCountryByIp = async (page: Page) => { + const data = await page.evaluate(() => { + return fetch('https://ipapi.co/json/') + .then(response => response.json()) + .then(data => data) + }) + return data.country; +} + +export const isIsraelIp = async (page: Page) => { + const country = await getCountryByIp(page); + return country === "IL"; } \ No newline at end of file diff --git a/reader/views.py b/reader/views.py index df04826270..3b8946893e 100644 --- a/reader/views.py +++ b/reader/views.py @@ -35,6 +35,7 @@ from sefaria.model import * from sefaria.google_storage_manager import GoogleStorageManager +from sefaria.model.text_reuqest_adapter import TextRequestAdapter from sefaria.model.user_profile import UserProfile, user_link, public_user_data, UserWrapper from sefaria.model.collection import CollectionSet from sefaria.model.webpage import get_webpages_for_ref @@ -250,6 +251,18 @@ def user_credentials(request): return {"user_type": "API", "user_id": apikey["uid"]} +def _reader_redirect_add_languages(request, tref): + versions = Ref(tref).version_list() + query_params = QueryDict(mutable=True) + for vlang, direction in [('ven', 'ltr'), ('vhe', 'rtl')]: + version_title = request.GET.get(vlang) + if version_title: + version_title = version_title.replace('_', ' ') + version = next((v for v in versions if v['direction'] == direction and v['versionTitle'] == version_title)) + query_params[vlang] = f'{version["languageFamilyName"]}|{version["versionTitle"]}' + return redirect(f'/{tref}/?{urllib.parse.urlencode(query_params)}') + + @ensure_csrf_cookie def catchall(request, tref, sheet=None): """ @@ -265,6 +278,10 @@ def reader_redirect(uref): response['Location'] += "?%s" % params if params else "" return response + for version in ['ven', 'vhe']: + if request.GET.get(version) and '|' not in request.GET.get(version): + return _reader_redirect_add_languages(request, tref) + if sheet is None: try: oref = Ref.instantiate_ref_with_legacy_parse_fallback(tref) @@ -290,7 +307,7 @@ def old_versions_redirect(request, tref, lang, version): def get_connections_mode(filter): # List of sidebar modes that can function inside a URL parameter to open the sidebar in that state. - sidebarModes = ("Sheets", "Notes", "About", "AboutSheet", "Navigation", "Translations", "Translation Open","WebPages", "extended notes", "Topics", "Torah Readings", "manuscripts", "Lexicon", "SidebarSearch", "Guide") + sidebarModes = ("Sheets", "Notes", "About", "AboutSheet", "Navigation", "Translations", "Translation Open", "Version Open", "WebPages", "extended notes", "Topics", "Torah Readings", "manuscripts", "Lexicon", "SidebarSearch", "Guide") if filter[0] in sidebarModes: return filter[0], True elif filter[0].endswith(" ConnectionsList"): @@ -300,7 +317,7 @@ def get_connections_mode(filter): else: return "TextList", False -def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **kwargs): +def make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, mode, **kwargs): """ Returns a dictionary corresponding to the React panel state, additionally setting `text` field with textual content. @@ -308,15 +325,8 @@ def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **k if oref.is_book_level(): index_details = library.get_index(oref.normal()).contents(with_content_counts=True) index_details["relatedTopics"] = get_topics_for_book(oref.normal(), annotate=True) - if kwargs.get('extended notes', 0) and (versionEn is not None or versionHe is not None): - currVersions = {"en": versionEn, "he": versionHe} - if versionEn is not None and versionHe is not None: - curr_lang = kwargs.get("panelDisplayLanguage", "en") - for key in list(currVersions.keys()): - if key == curr_lang: - continue - else: - currVersions[key] = None + if kwargs.get('extended notes', 0): + currVersions = {"en": translationVersion, "he": primaryVersion} panel = { "menuOpen": "extended notes", "mode": "Menu", @@ -339,10 +349,7 @@ def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **k "mode": mode, "ref": oref.normal(), "refs": [oref.normal()] if not oref.is_spanning() else [r.normal() for r in oref.split_spanning_ref()], - "currVersions": { - "en": versionEn, - "he": versionHe, - }, + "currVersions": {"en": translationVersion, "he": primaryVersion}, "filter": filter, "versionFilter": versionFilter, } @@ -373,15 +380,29 @@ def make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, mode, **k if settings_override: panel["settings"] = settings_override if mode != "Connections" and oref != None: - try: - text_family = TextFamily(oref, version=panel["currVersions"]["en"], lang="en", version2=panel["currVersions"]["he"], lang2="he", commentary=False, - context=True, pad=True, alts=True, wrapLinks=False, translationLanguagePreference=kwargs.get("translationLanguagePreference", None)).contents() - except NoVersionFoundError: - text_family = {} - text_family["updateFromAPI"] = True - text_family["next"] = oref.next_section_ref().normal() if oref.next_section_ref() else None - text_family["prev"] = oref.prev_section_ref().normal() if oref.prev_section_ref() else None - panel["text"] = text_family + primary_params = [primaryVersion['languageFamilyName'], primaryVersion['versionTitle']] + primary_params[0] = primary_params[0] or 'primary' + translation_params = [translationVersion['languageFamilyName'], translationVersion['versionTitle']] + translation_params[0] = translation_params[0] or 'translation' + text_adapter = TextRequestAdapter(oref.section_ref(), [primary_params, translation_params], return_format='wrap_all_entities') + text = text_adapter.get_versions_for_query() + # text['ref'] = oref.normal() + #now we we should add the he and text attributes + if len(text['versions']) == 2: + if text['versions'][0]['isPrimary'] and not text['versions'][1]['isSource']: + text['he'], text['text'] = text['versions'][0]['text'], text['versions'][1]['text'] + else: + text['he'], text['text'] = text['versions'][1]['text'], text['versions'][0]['text'] + elif len(text['versions']) == 1: + if primary_params == translation_params: + text['he'] = text['text'] = text['versions'][0]['text'] + elif [text['versions'][0]['languageFamilyName'], text['versions'][0]['versionTitle']] == translation_params: + text['text'], text['he'] = text['versions'][0]['text'], [] + else: + text['he'], text['text'] = text['versions'][0]['text'], [] + + text["updateFromAPI"] = True + panel["text"] = text if oref.index.categories == ["Tanakh", "Torah"]: panel["indexDetails"] = oref.index.contents() # Included for Torah Parashah titles rendered in text @@ -460,7 +481,7 @@ def make_sheet_panel_dict(sheet_id, filter, **kwargs): return panels -def make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_panel, **kwargs): +def make_panel_dicts(oref, primaryVersion, translationVersion, filter, versionFilter, multi_panel, **kwargs): """ Returns an array of panel dictionaries. Depending on whether `multi_panel` is True, connections set in `filter` are displayed in either 1 or 2 panels. @@ -468,16 +489,22 @@ def make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_pa panels = [] # filter may have value [], meaning "all". Therefore we test filter with "is not None". if filter is not None and multi_panel: - panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Text", **kwargs)] - panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Connections", **kwargs)] + panels += [make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, "Text", **kwargs)] + panels += [make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, "Connections", **kwargs)] elif filter is not None and not multi_panel: - panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "TextAndConnections", **kwargs)] + panels += [make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, "TextAndConnections", **kwargs)] else: - panels += [make_panel_dict(oref, versionEn, versionHe, filter, versionFilter, "Text", **kwargs)] + panels += [make_panel_dict(oref, primaryVersion, translationVersion, filter, versionFilter, "Text", **kwargs)] return panels +def _extract_version_params(request, key): + params = request.GET.get(key, '|') + params = params.replace("_", " ") + languageFamilyName, versionTitle = params.split('|') + return {'languageFamilyName': languageFamilyName, 'versionTitle': versionTitle} + @sanitize_get_params def text_panels(request, ref, version=None, lang=None, sheet=None): """ @@ -495,12 +522,8 @@ def text_panels(request, ref, version=None, lang=None, sheet=None): panels = [] multi_panel = not request.user_agent.is_mobile and not "mobile" in request.GET # Handle first panel which has a different signature in params - versionEn = request.GET.get("ven", None) - if versionEn: - versionEn = versionEn.replace("_", " ") - versionHe = request.GET.get("vhe", None) - if versionHe: - versionHe = versionHe.replace("_", " ") + primaryVersion = _extract_version_params(request, 'vhe') + translationVersion = _extract_version_params(request, 'ven') filter = request.GET.get("with").replace("_", " ").split("+") if request.GET.get("with") else None filter = [] if filter == ["all"] else filter @@ -510,11 +533,7 @@ def text_panels(request, ref, version=None, lang=None, sheet=None): if sheet == None: versionFilter = [request.GET.get("vside").replace("_", " ")] if request.GET.get("vside") else [] - if versionEn and not Version().load({"versionTitle": versionEn, "language": "en"}): - raise Http404 - if versionHe and not Version().load({"versionTitle": versionHe, "language": "he"}): - raise Http404 - versionEn, versionHe = override_version_with_preference(oref, request, versionEn, versionHe) + # versionEn, versionHe = override_version_with_preference(oref, request, versionEn, versionHe) #TODO kwargs = { "panelDisplayLanguage": request.GET.get("lang", request.contentLang), @@ -534,7 +553,7 @@ def text_panels(request, ref, version=None, lang=None, sheet=None): kwargs["sidebarSearchQuery"] = request.GET.get("sbsq", None) kwargs["selectedNamedEntity"] = request.GET.get("namedEntity", None) kwargs["selectedNamedEntityText"] = request.GET.get("namedEntityText", None) - panels += make_panel_dicts(oref, versionEn, versionHe, filter, versionFilter, multi_panel, **kwargs) + panels += make_panel_dicts(oref, primaryVersion, translationVersion, filter, versionFilter, multi_panel, **kwargs) elif sheet == True: panels += make_sheet_panel_dict(ref, filter, **{"panelDisplayLanguage": request.GET.get("lang",request.contentLang), "referer": request.path}) diff --git a/sefaria/datatype/jagged_array.py b/sefaria/datatype/jagged_array.py index 5d35642afb..3957960061 100644 --- a/sefaria/datatype/jagged_array.py +++ b/sefaria/datatype/jagged_array.py @@ -659,12 +659,13 @@ def modify_by_function(self, func, start_sections=None, _cur=None, _curSections= Func should accept two parameters: 1) text of current segment 2) zero-indexed indices of segment :param start_sections: array(int), optional param. Sections passed to `func` will be offset by `start_sections`, if passed """ - _curSections = _curSections or [] if _cur is None: _cur = self._store if isinstance(_cur, str): + _curSections = _curSections or [0] return func(_cur, self.get_offset_sections(_curSections, start_sections)) elif isinstance(_cur, list): + _curSections = _curSections or [] return [self.modify_by_function(func, start_sections, temp_curr, _curSections + [i]) for i, temp_curr in enumerate(_cur)] def flatten_to_array(self, _cur=None): diff --git a/sefaria/model/dependencies.py b/sefaria/model/dependencies.py index eb842a16bb..2a186610f2 100644 --- a/sefaria/model/dependencies.py +++ b/sefaria/model/dependencies.py @@ -57,7 +57,7 @@ def process_version_title_change_in_search(ver, **kwargs): text_index = library.get_index(ver.title) delete_version(text_index, kwargs.get("old"), ver.language) for ref in text_index.all_segment_refs(): - TextIndexer.index_ref(search_index_name, ref, kwargs.get("new"), ver.language, False) + TextIndexer.index_ref(search_index_name, ref, kwargs.get("new"), ver.language, ver.languageFamilyName, ver.isPrimary) # Version Title Change diff --git a/sefaria/model/text_reuqest_adapter.py b/sefaria/model/text_reuqest_adapter.py index d98aa31ba1..3030522300 100644 --- a/sefaria/model/text_reuqest_adapter.py +++ b/sefaria/model/text_reuqest_adapter.py @@ -98,7 +98,6 @@ def _add_ref_data_to_return_obj(self) -> None: 'heSectionRef': oref.section_ref().he_normal(), 'firstAvailableSectionRef': oref.first_available_section_ref().normal(), 'isSpanning': oref.is_spanning(), - 'spanningRefs': [r.normal() for r in oref.split_spanning_ref()], 'next': oref.next_section_ref().normal() if oref.next_section_ref() else None, 'prev': oref.prev_section_ref().normal() if oref.prev_section_ref() else None, 'title': oref.context_ref().normal(), @@ -107,6 +106,8 @@ def _add_ref_data_to_return_obj(self) -> None: 'primary_category': oref.primary_category, 'type': oref.primary_category, #same as primary category }) + if self.return_obj['isSpanning']: + self.return_obj['spanningRefs'] = [r.normal() for r in oref.split_spanning_ref()] def _add_index_data_to_return_obj(self) -> None: index = self.oref.index diff --git a/sefaria/search.py b/sefaria/search.py index ef534422ed..f9728a8f57 100644 --- a/sefaria/search.py +++ b/sefaria/search.py @@ -541,7 +541,7 @@ def index_version(cls, version, tries=0, action=None): print("Could not find dictionary node in {}".format(version.title)) @classmethod - def index_ref(cls, index_name, oref, version_title, lang): + def index_ref(cls, index_name, oref, version_title, lang, language_family_name, is_primary): # slower than `cls.index_version` but useful when you don't want the overhead of loading all versions into cache cls.index_name = index_name cls.curr_index = oref.index @@ -558,7 +558,7 @@ def index_ref(cls, index_name, oref, version_title, lang): content = TextChunk(oref, lang, vtitle=version_title).ja().flatten_to_string() categories = cls.curr_index.categories tref = oref.normal() - doc = cls.make_text_index_document(tref, oref.he_normal(), version_title, lang, version_priority, content, categories, hebrew_version_title) + doc = cls.make_text_index_document(tref, oref.he_normal(), version_title, lang, version_priority, content, categories, hebrew_version_title, language_family_name, is_primary) id = make_text_doc_id(tref, version_title, lang) es_client.index(index_name, doc, id=id) @@ -567,11 +567,13 @@ def _cache_action(cls, segment_str, tref, heTref, version): # Index this document as a whole vtitle = version.versionTitle vlang = version.language + language_family_name = version.languageFamilyName + is_primary = version.isPrimary hebrew_version_title = getattr(version, 'versionTitleInHebrew', None) try: version_priority, categories = cls.version_priority_map[(version.title, vtitle, vlang)] #TODO include sgement_str in this func - doc = cls.make_text_index_document(tref, heTref, vtitle, vlang, version_priority, segment_str, categories, hebrew_version_title) + doc = cls.make_text_index_document(tref, heTref, vtitle, vlang, version_priority, segment_str, categories, hebrew_version_title, language_family_name, is_primary) # print doc except Exception as e: logger.error("Error making index document {} / {} / {} : {}".format(tref, vtitle, vlang, str(e))) @@ -613,7 +615,7 @@ def modify_text_in_doc(cls, content): return content @classmethod - def make_text_index_document(cls, tref, heTref, version, lang, version_priority, content, categories, hebrew_version_title): + def make_text_index_document(cls, tref, heTref, version, lang, version_priority, content, categories, hebrew_version_title, language_family_name, is_primary): """ Create a document for indexing from the text specified by ref/version/lang """ @@ -653,6 +655,8 @@ def make_text_index_document(cls, tref, heTref, version, lang, version_priority, "exact": content, "naive_lemmatizer": content, 'hebrew_version_title': hebrew_version_title, + "languageFamilyName": language_family_name, + "isPrimary": is_primary, } @@ -734,7 +738,7 @@ def index_from_queue(): queue = db.index_queue.find() for item in queue: try: - TextIndexer.index_ref(index_name, Ref(item["ref"]), item["version"], item["lang"]) + TextIndexer.index_ref(index_name, Ref(item["ref"]), item["version"], item["lang"], item['languageFamilyName'], item['isPrimary']) db.index_queue.remove(item) except Exception as e: logging.error("Error indexing from queue ({} / {} / {}) : {}".format(item["ref"], item["version"], item["lang"], e)) diff --git a/sourcesheets/views.py b/sourcesheets/views.py index 5f25f971fc..65c9de1c69 100644 --- a/sourcesheets/views.py +++ b/sourcesheets/views.py @@ -2,7 +2,7 @@ import json import httplib2 from urllib3.exceptions import NewConnectionError -from urllib.parse import unquote +from urllib.parse import unquote, urlencode from elasticsearch.exceptions import AuthorizationException from datetime import datetime from io import StringIO, BytesIO @@ -202,6 +202,15 @@ def view_sheet(request, sheet_id, editorMode = False): editor = request.GET.get('editor', '0') embed = request.GET.get('embed', '0') + interface_lang = request.interfaceLang + content_lang = request.GET.get("lang", request.contentLang) + fixed_content_lang = 'he' if interface_lang == 'hebrew' else 'bi' + if content_lang != fixed_content_lang: + query_params = request.GET.dict() + query_params['lang'] = fixed_content_lang + new_url = f"/sheets/{sheet_id}?{urlencode(query_params)}" + return redirect(new_url, permanent=True) + if editor != '1' and embed !='1' and editorMode is False: return catchall(request, sheet_id, True) diff --git a/static/css/s2.css b/static/css/s2.css index 1adb3a704f..525cc2daf1 100644 --- a/static/css/s2.css +++ b/static/css/s2.css @@ -1543,9 +1543,19 @@ div.interfaceLinks-row a { .readerPanel.bilingual .readerNavMenu .gridBox { direction: ltr; } -.readerPanel.english .he { +.readerPanel.english .contentText .he, +.readerPanel.hebrew .contentText .en, +.readerPanel.english .contentSpan.primary, +.readerPanel.english .languageToggle .he, +.readerPanel.hebrew .contentSpan.translation, +.readerPanel.hebrew .languageToggle .en { display: none; } +.readerPanel.english .versionsTextList .primary, +.readerPanel.hebrew .versionsTextList .translation { + display: block; +} + .readerPanel.english .he.heOnly{ display: inline; text-align: right; @@ -1557,11 +1567,10 @@ div.interfaceLinks-row a { display: inline; text-align: right; } -.readerPanel.hebrew .en { - display: none; -} .readerPanel.english .heOnly .he, -.readerPanel.bilingual .heOnly .he { +.readerPanel.bilingual .heOnly .he, +.readerPanel.english .enOnly .en, +.readerPanel.bilingual .enOnly .en { display: inline; } .languageToggle { @@ -4187,6 +4196,9 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item:hover{ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus { text-decoration: underline; } +.singlePanel .searchContent h1 { + height: revert; +} .searchContent h1 { height: 40px; font-size: 30px; @@ -4930,9 +4942,19 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus display: flex; justify-content: space-between; padding: 20px 0; +} + +.readerNavMenu .sheet:not(.profile-page .sheet) { border-bottom: 1px solid #eee; color: black; } + +.sheetsProfileList .sheet { + display: flex; + justify-content: space-between; + padding: 20px 0; +} + .readerNavMenu .sheet:hover{ text-decoration: none; } @@ -4987,6 +5009,10 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus .tagsList .enOnly { direction: ltr; } +.readerControlsOuter { + position: relative; + z-index: 103; +} .readerControls { position: relative; top: 0; @@ -4994,7 +5020,6 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus width: 100%; box-sizing: border-box; text-align: center; - z-index: 100; height: 60px; line-height: 60px; box-shadow: 0 1px 3px rgba(0,0,0,0.2); @@ -5023,13 +5048,14 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus background-color: #EDEDED; } .readerControls .connectionsPanelHeader .connectionsHeaderTitle { - text-transform: uppercase; letter-spacing: 1px; font-size: 16px; font-weight: lighter; } +.readerControls .connectionsPanelHeader .connectionsHeaderTitle:not(.active) { + text-transform: uppercase; +} .connectionsPanelHeader .connectionsHeaderTitle.active { - text-transform: none; cursor: pointer; } .connectionsHeaderTitle .fa-chevron-left { @@ -5059,7 +5085,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus } .readerControls.transLangPrefSuggBann { background-color: #EDEDEC; - z-index: 99; + z-index: 2; } .readerControls .readerControlsInner.transLangPrefSuggBannInner { justify-content: center; @@ -5149,7 +5175,6 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus } .readerControls.connectionsHeader .readerTextToc { font-family: "Roboto", "Helvetica Neue", "Helvetica", sans-serif; - text-transform: uppercase; color: #666; width: 100%; } @@ -5181,6 +5206,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus display: flex; flex-direction: row; text-align: right; + align-items: center; } /* icons need a little nudge in flipped hebrew mode */ .interface-hebrew .rightButtons { @@ -5598,7 +5624,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus .bilingual .sheetContent .title .he { display: none; } -.interface-hebrew .readerPanel.english .textRange, +.interface-hebrew .readerPanel.ltr .textRange, .interface-hebrew .readerPanel.english .categoryFilterGroup, .interface-hebrew .readerPanel.bilingual .categoryFilterGroup, .interface-hebrew .readerPanel.english .essayGroup, @@ -5607,6 +5633,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus .interface-hebrew .readerPanel.bilingual .textTableOfContents { direction: ltr; } +.interface-english .readerPanel.rtl .textRange, .interface-english .readerPanel.hebrew .textRange, .interface-english .readerPanel.hebrew .categoryFilterGroup, .interface-english .readerPanel.hebrew .essayGroup, @@ -5715,7 +5742,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus display: inline; }*/ .bilingual .segment > .he, -.bilingual .segment > p > .he{ +.bilingual .segment > p > .he { display: block; } .bilingual .segment > .en, @@ -5743,11 +5770,14 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus .stacked.bilingual .sheetContent .segment > p > .en { margin-top: 0; } -.stacked.bilingual .basetext .segment > .en , -.stacked.bilingual .basetext .segment > p > .en { +.stacked.bilingual .basetext .segment > .translation , +.stacked.bilingual .basetext .segment > p > .translation { margin: 10px 0 20px; color: #666; } +.stacked.bilingual .basetext .segment > p > .he.translation { + color: black; +} .stacked.bilingual .segment.heOnly > .he, .stacked.bilingual .segment.enOnly > .en, .stacked.bilingual .segment.heOnly > p > .he, @@ -5802,20 +5832,28 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus .readerPanel.english .SheetSource .sheetItem.segment .en { background-color: white; } -.heLeft.bilingual .segment > .en, -.heRight.bilingual .segment > .he , -.heLeft.bilingual .segment > p > .en, -.heRight.bilingual .segment > p > .he { +.heLeft.bilingual .segment > .translation, +.heRight.bilingual .segment > .primary, +.heLeft.bilingual .segment > p > .translation, +.heRight.bilingual .segment > p > .primary, +.heRight.bilingual .sheetItem.segment > .he, +.heLeft.bilingual .sheetItem.segment > .en{ float: right; padding-left: 20px; } -.heRight.bilingual .segment > .en, -.heLeft.bilingual .segment > .he, -.heRight.bilingual .segment > p > .en, -.heLeft.bilingual .segment > p > .he { +.heRight.bilingual .segment > .translation, +.heLeft.bilingual .segment > .primary, +.heRight.bilingual .segment > p > .translation, +.heLeft.bilingual .segment > p > .primary, +.heRight.bilingual .sheetItem.segment > .en, +.heLeft.bilingual .sheetItem.segment > .he{ float: left; padding-right: 20px; } +.segment > p > .he.translation { + --hebrew-font: var(--hebrew-sans-serif-font-family); + font-size: 100%; +} .basetext .segment:active, .basetext .segment:focus { background-color: #f5faff; @@ -5904,6 +5942,7 @@ body .ui-autocomplete.dictionary-toc-autocomplete .ui-menu-item a.ui-state-focus .segment.heOnly .en{ display: none; } +/*in the text reader we don't have enOnly anymore. it always hvae primary (which is one the meaning of heOnly) maybe this is useful for other cases*/ .segment.enOnly .he{ display: none; } @@ -7987,7 +8026,9 @@ But not to use a display block directive that might break continuous mode for ot --hebrew-font: var(--hebrew-sans-serif-font-family); border: none; } - +.saveProfileButton{ + background-color: var(--midrash-green); +} .button.disabled{ border: 1px solid var(--light-grey); background-color: var(--lightest-grey); @@ -8421,9 +8462,19 @@ a .button:hover { display: flex; flex-direction: row; justify-content: space-between; + } .profile-page .collectionListing { padding: 20px 0; + margin-top: 20px; + background-color: white; + border: 1000px solid white; + border-top: 4px solid white; + border-bottom: 120px solid white; + margin: -4px 0 -120px 0; + margin-inline-start: -1000px; + margin-inline-end: 0; + width: 100%; } .collectionListing .left-content { display: flex; @@ -8787,6 +8838,9 @@ body.interface-english .publishBox .react-tags__suggestions ul { .collectionListingDetails { color: #999; font-size: 14px; + font-family: Roboto !important; + font-weight: 400; + line-height: 18px; } .collectionListingMembership { text-transform: capitalize; @@ -8896,7 +8950,10 @@ body.interface-english .publishBox .react-tags__suggestions ul { flex: 1; } .sheet .sheetTitleText { - font-family: var(--english-serif-font-family); + font-family: Roboto; + font-size: 16px; + font-weight: 600; + line-height: 18.75px; } .sheetListingPinButton { display: none; @@ -10834,11 +10891,14 @@ section.SheetOutsideBiText { .readerPanel.hebrew section.SheetSource .sheetItem > .he, .readerPanel.english section.SheetSource .sheetItem > .en, +.readerPanel.hebrew section.SheetSource .sheetItem.enOnly > .en > .sourceContentText, .readerPanel.hebrew section.SheetOutsideBiText .sheetItem > .he, .readerPanel.english section.SheetOutsideBiText .sheetItem > .en { display: block; } -.readerPanel.hebrew section.SheetSource .sheetItem > .en, +.readerPanel.hebrew section.SheetSource .sheetItem:not(.enOnly) > .en, +.readerPanel.hebrew section.SheetSource .sheetItem.enOnly > .en > .ref, +.readerPanel.hebrew section.SheetSource .sheetItem.enOnly > .he > .sourceContentText, .readerPanel.english section.SheetSource .sheetItem > .he, .readerPanel.hebrew section.SheetOutsideBiText .sheetItem > .en, .readerPanel.english section.SheetOutsideBiText .sheetItem > .he { @@ -11512,8 +11572,8 @@ cursor: pointer; .sheetList .sheet .sheetTags { color: #999; } -.sheetList .sheet .sheetTags .bullet { - margin: 0 5px; +.sheetsProfileList .sheet .sheetTags .bullet { + padding: 0 5px; } .sheetList .sheet a { color: inherit; @@ -11649,8 +11709,12 @@ cursor: pointer; top: -2px; } .profile-page .collectionListingName { - font-size: 18px; margin-bottom: 10px; + font-family: Roboto; + font-size: 16px; + font-weight: 600; + line-height: 18.75px; + } .profile-page .collectionListing + .collectionListing { border-top: 0; @@ -11699,6 +11763,10 @@ cursor: pointer; margin-top: 20px; font-size: 18px; line-height: 1.4; + font-family: Roboto; + font-size: 14px; + font-weight: 400; + line-height: 16.41px; } @media (max-width: 540px) { .profile-page .profile-pic:hover .profile-pic-hover-button.profile-pic-button-visible { @@ -11721,7 +11789,9 @@ cursor: pointer; font-size: 43.5px !important; } .profile-page .profile-summary .profile-actions { + display: flex; flex-wrap: wrap; + flex-direction: row; } } .profile-page .profile-summary .follow { @@ -11732,11 +11802,7 @@ cursor: pointer; .profile-page .profile-summary .follow .follow-bull { padding: 0 5px; } -.profile-page .profile-summary .profile-actions { - display: flex; - flex-direction: row; - margin: 10px 0; -} + .profile-page .resourcesLink.faded { color: #666; } @@ -13580,6 +13646,224 @@ span.ref-link-color-3 {color: blue} cursor: pointer; } +.readerDropdownMenu { + position: relative; + display: flex; + flex-direction: row-reverse; + z-index: 3; +} + +.dropdownLinks-menu { + display: contents; +} + +.dropdownLinks-menu.closed { + display: none; +} + +.texts-properties-menu { + width: 256px; + border: 1px solid var(--lighter-grey); + border-radius: 5px; + box-shadow: 0px 2px 4px var(--lighter-grey); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: absolute; + top: 100%; + background-color: white; + --english-font: var(--english-sans-serif-font-family); + --hebrew-font: var(--hebrew-sans-serif-font-family); +} + +.dropdownLinks-button { + border: none; + background-color: inherit; +} + +.rightButtons .dropdownLinks-button { + text-align: end; +} + +.toggle-switch-container { + align-items: center; + display: flex; + direction: ltr; +} + +.toggle-switch { + position: relative; + width: 46px; + display: inline-block; + text-align: left; +} + +.toggle-switch-checkbox { + display: none; +} + +.toggle-switch-label { + display: block; + overflow: hidden; + cursor: pointer; + border: 0 solid var(--light-grey); + border-radius: 20px; +} + +.toggle-switch-inner { + display: block; + width: 200%; + margin-left: -100%; + transition: margin 0.3s ease-in 0s; +} + +.toggle-switch-inner:before, +.toggle-switch-inner:after { + float: left; + width: 50%; + height: 24px; + padding: 0; + line-height: 24px; + color: white; + font-weight: bold; + box-sizing: border-box; + content: ""; + color: white; +} + +.toggle-switch-inner:before { + padding-left: 10px; + background-color: var(--sefaria-blue); +} + +.toggle-switch-inner:after { + padding-right: 10px; + background-color: var(--light-grey); +} + +.toggle-switch-switch { + display: block; + width: 20px; + height: 20px; + background: white; + position: absolute; + top: 50%; + bottom: 0; + right: 24px; + border: 0 solid var(--light-grey); + border-radius: 20px; + transition: all 0.3s ease-in 0s; + transform: translateY(-50%); +} + +.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-inner { + margin-left: 0; +} + +.toggle-switch-checkbox:checked + .toggle-switch-label .toggle-switch-switch { + right: 2px; +} + +.toggle-switch-checkbox:disabled + .toggle-switch-label .toggle-switch-inner:after { + background-color: var(--lighter-grey); +} + +.toggle-switch-line { + display: flex; + width: 216px; + height: 49px; + justify-content: space-between; + align-items: center; +} + +.toggle-switch-line:is(.disabled) .int-en, +.toggle-switch-line:is(.disabled) .int-he { + color: var(--light-grey); +} + +.texts-properties-menu .int-en, +.texts-properties-menu .int-he { + align-content: center; +} + +.show-source-translation-buttons { + display: flex; + flex-direction: column; + height: 170px; + justify-content: center; +} + +.show-source-translation-buttons .button { + margin: unset; + display: flex; + height: 46px; + width: 235px; + align-items: center; + justify-content: center; + margin: 3px 0; +} + +.show-source-translation-buttons .button:not(.checked) { + background-color: var(--lighter-grey); + color: black; +} + +.layout-button-line { + height: 57px; + width: 216px; + display: flex; + justify-content: space-between; + align-items: center; +} + +.layout-options { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; +} + +.layout-button { + border: none; + width: 28px; + height: 24px; + -webkit-mask: var(--url) no-repeat; + -webkit-mask-size: contain; + -webkit-mask-repeat: no-repeat; + -webkit-mask-position: center; + -webkit-mask-size: 100% 100%; + background-color: var(--medium-grey); + cursor: pointer; +} + +.layout-button.checked { + background-color: var(--sefaria-blue); +} + +.text-menu-border { + width: 100%; + height: 1px; + background-color: var(--lighter-grey); +} + +.font-size-line { + width: 230px; + height: 50px; + display: flex; + justify-content: space-between; + align-items: center; + direction: ltr; +} + +.font-size-button { + display: flex; + align-items: center; + background-color: white; + border: none; + cursor: pointer; +} + #currentlyReadingContainer { margin: 5px 30px; flex-grow: 1; @@ -13758,7 +14042,7 @@ span.ref-link-color-3 {color: blue} .dropdownSeparator { border: 1px solid var(--light-grey); } -.header .dropdownLinks { +.header .headerDropdownMenu { display: block; align-items: center; margin-top: 2px; @@ -13770,21 +14054,21 @@ span.ref-link-color-3 {color: blue} -moz-margin-start: 20px; margin-inline-start: 10px; } -.header .dropdownLinks img { +.header .headerDropdownMenu img { height: 18px; width: 18px; vertical-align: middle; } -.interface-english .header .dropdownLinks img { +.interface-english .header .headerDropdownMenu img { margin-inline-end: 2px; } -.interface-hebrew .header .dropdownLinks img { +.interface-hebrew .header .headerDropdownMenu img { margin-inline-end: 6px; } -/* .header .dropdownLinks a.dropdownLinks-button::after { +/* .header .headerDropdownMenu a.dropdownLinks-button::after { display: inline-block; background: no-repeat url("/static/icons/chevron-down.svg"); height: 10px; @@ -13793,7 +14077,7 @@ span.ref-link-color-3 {color: blue} content: ""; transform: scale(0.75); } */ -.header .dropdownLinks .dropdownLinks-menu { +.header .headerDropdownMenu .dropdownLinks-menu { top: 35px; position: absolute; z-index: 1; @@ -13804,12 +14088,12 @@ span.ref-link-color-3 {color: blue} display:none; overflow:hidden; } -.interface-english .header .dropdownLinks .dropdownLinks-menu { +.interface-english .header .headerDropdownMenu .dropdownLinks-menu { right: 0px; min-width: 150px; width: max-content; } -.interface-hebrew .header .dropdownLinks .dropdownLinks-menu { +.interface-hebrew .header .headerDropdownMenu .dropdownLinks-menu { left: 0px; min-width: 150px; width: max-content; @@ -13820,7 +14104,7 @@ span.ref-link-color-3 {color: blue} padding: 4px; max-width: 220px; } -.header .dropdownLinks .dropdownLinks-menu.open { +.header .headerDropdownMenu .dropdownLinks-menu.open { display: block; } @@ -14113,6 +14397,57 @@ span.ref-link-color-3 {color: blue} padding: 0px 4px; } +.profilePicAndButtonContainer { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.createButtons { + display: flex; + align-items: center; + flex-direction: row; + justify-content: space-evenly; + padding-top: 20px; +} + +.sheetsProfileList { + margin-top: 20px; + background-color: white; + border: 1000px solid white; + border-top: 4px solid white; + border-bottom: 120px solid white; + margin: -4px 0 -120px -1000px; + width: 100%; + +} + +.profile-page .sheetsProfileList { + margin: -4px 0 -120px 0; + margin-inline-start: -1000px; + margin-inline-end: 0; +} + +.sheetsProfileList .readerNavMenuSearchButton { + margin: 0 10px 0 5px; + display: inline-flex; + top: 0; +} +.sheetsProfileList .readerNavMenuSearchButton { + margin-inline-start: 10px; + margin-inline-end: 5px; +} +.sheetsProfileList input { + border: 0; + font-size: 18px; + font-family: "adobe-garamond-pro", "Crimson Text", Georgia, serif; + background-color: transparent; +} +.sheetsProfileList .loadingMessage { + margin-top: 30px; +} + @-webkit-keyframes load5 { 0%,100%{box-shadow:0 -2.6em 0 0 #ffffff,1.8em -1.8em 0 0 rgba(0,0,0,0.2),2.5em 0 0 0 rgba(0,0,0,0.2),1.75em 1.75em 0 0 rgba(0,0,0,0.2),0 2.5em 0 0 rgba(0,0,0,0.2),-1.8em 1.8em 0 0 rgba(0,0,0,0.2),-2.6em 0 0 0 rgba(0,0,0,0.5),-1.8em -1.8em 0 0 rgba(0,0,0,0.7)} 12.5%{box-shadow:0 -2.6em 0 0 rgba(0,0,0,0.7),1.8em -1.8em 0 0 #ffffff,2.5em 0 0 0 rgba(0,0,0,0.2),1.75em 1.75em 0 0 rgba(0,0,0,0.2),0 2.5em 0 0 rgba(0,0,0,0.2),-1.8em 1.8em 0 0 rgba(0,0,0,0.2),-2.6em 0 0 0 rgba(0,0,0,0.2),-1.8em -1.8em 0 0 rgba(0,0,0,0.5)} diff --git a/static/css/static.css b/static/css/static.css index 1459cd84f3..d5cc7756de 100644 --- a/static/css/static.css +++ b/static/css/static.css @@ -858,11 +858,6 @@ p.registration-links a:hover{ #editProfilePage #sheetsBox, #editProfilePage #aboutBox { margin-bottom: 60px; - display: flex; - display: -webkit-flex; - justify-content: space-between; - -webkit-justify-content:space-between; - flex-flow: row wrap; } #editProfilePage #sheetsBox{ flex-direction: column; @@ -906,7 +901,6 @@ p.registration-links a:hover{ } #editProfilePage .field { margin-bottom: 20px; - width: 48%; min-width: 300px; } #editProfilePage .field input { diff --git a/static/icons/bi-ltr-heLeft.svg b/static/icons/bi-ltr-heLeft.svg new file mode 100644 index 0000000000..2d28ee586f --- /dev/null +++ b/static/icons/bi-ltr-heLeft.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/bi-ltr-stacked.svg b/static/icons/bi-ltr-stacked.svg new file mode 100644 index 0000000000..27fb13f535 --- /dev/null +++ b/static/icons/bi-ltr-stacked.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/bi-rtl-heRight.svg b/static/icons/bi-rtl-heRight.svg new file mode 100644 index 0000000000..67a7c095d0 --- /dev/null +++ b/static/icons/bi-rtl-heRight.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/bi-rtl-stacked.svg b/static/icons/bi-rtl-stacked.svg new file mode 100644 index 0000000000..035734e83f --- /dev/null +++ b/static/icons/bi-rtl-stacked.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/enlarge_font.svg b/static/icons/enlarge_font.svg new file mode 100644 index 0000000000..3c1ff999d6 --- /dev/null +++ b/static/icons/enlarge_font.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/mixed-beside-ltrrtl.svg b/static/icons/mixed-beside-ltrrtl.svg new file mode 100644 index 0000000000..f19de475c1 --- /dev/null +++ b/static/icons/mixed-beside-ltrrtl.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/mixed-beside-rtlltr.svg b/static/icons/mixed-beside-rtlltr.svg new file mode 100644 index 0000000000..a35ebe96f5 --- /dev/null +++ b/static/icons/mixed-beside-rtlltr.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/mixed-stacked-ltrrtl.svg b/static/icons/mixed-stacked-ltrrtl.svg new file mode 100644 index 0000000000..a0d8d6bb75 --- /dev/null +++ b/static/icons/mixed-stacked-ltrrtl.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/mixed-stacked-rtlltr.svg b/static/icons/mixed-stacked-rtlltr.svg new file mode 100644 index 0000000000..3e088068f8 --- /dev/null +++ b/static/icons/mixed-stacked-rtlltr.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/mono-continuous.svg b/static/icons/mono-continuous.svg new file mode 100644 index 0000000000..f84c2f3fa8 --- /dev/null +++ b/static/icons/mono-continuous.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/mono-segmented.svg b/static/icons/mono-segmented.svg new file mode 100644 index 0000000000..9f1b6bffa9 --- /dev/null +++ b/static/icons/mono-segmented.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/reduce_font.svg b/static/icons/reduce_font.svg new file mode 100644 index 0000000000..0714abe7fb --- /dev/null +++ b/static/icons/reduce_font.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/js/AboutBox.jsx b/static/js/AboutBox.jsx index 1ede90b49b..a0e2040944 100644 --- a/static/js/AboutBox.jsx +++ b/static/js/AboutBox.jsx @@ -3,8 +3,9 @@ import PropTypes from 'prop-types'; import Sefaria from './sefaria/sefaria'; import VersionBlock, {VersionsBlocksList} from './VersionBlock/VersionBlock'; import Component from 'react-class'; -import {InterfaceText} from "./Misc"; +import {InterfaceText, LoadingMessage} from "./Misc"; import {ContentText} from "./ContentText"; +import {VersionsTextList} from "./VersionsTextList"; import { SidebarModules } from './NavSidebar'; @@ -63,8 +64,8 @@ class AboutBox extends Component { this.setState({versionLangMap: versionsByLang, currentVersionsByActualLangs:currentVersionsByActualLangs}); } openVersionInSidebar(versionTitle, versionLanguage) { - this.props.setConnectionsMode("Translation Open", {previousMode: "About"}); - this.props.setFilter(Sefaria.getTranslateVersionsKey(versionTitle, versionLanguage)); + this.props.setConnectionsMode("Version Open", {previousMode: "About"}); + this.props.setFilter(Sefaria.getTranslateVersionsKey(versionTitle, versionLanguage), 'About'); } isSheet(){ return this.props.srefs[0].startsWith("Sheet"); @@ -94,14 +95,36 @@ class AboutBox extends Component { return
{detailSection}
; } + if (!Object.keys(this.state.versionLangMap).length) { + return ( +
+ +
+ ); + } + + if (this.props.mode === "Version Open") { + return ( + + ); + } + const category = Sefaria.index(this.state?.details?.title)?.primary_category; const isDictionary = d?.lexiconName; - const sourceVersion = this.state.currentVersionsByActualLangs?.he; - const translationVersions = Object.entries(this.state.currentVersionsByActualLangs).filter(([lang, version]) => lang != "he").map(([lang, version])=> version); - const multiple_translations = translationVersions?.length > 1; - const no_source_versions = multiple_translations || translationVersions?.length == 1 && !sourceVersion; + const sourceVersion = this.props.currObjectVersions.he; + const translationVersion = this.props.currObjectVersions?.en; + const no_source_versions = !sourceVersion; const sourceVersionSectionTitle = {en: "Current Version", he:"מהדורה נוכחית"}; - const translationVersionsSectionTitle = multiple_translations ? {en: "Current Translations", he:"תרגומים נוכחיים"} : {en: "Current Translation", he:"תרגום נוכחי"}; + const translationVersionSectionTitle = {en: "Current Translation", he:"תרגום נוכחי"}; const alternateVersionsSectionTitle = no_source_versions ? {en: "Source Versions", he:"מהדורות בשפת המקור"} : {en: "Alternate Source Versions", he:"מהדורות נוספות בשפת המקור"} let detailSection = null; @@ -163,8 +186,10 @@ class AboutBox extends Component { { !!placeTextEn || !!dateTextEn ?
- {`Composed: ${!!placeTextEn ? placeTextEn : ""} ${!!dateTextEn ? dateTextEn : ""}`} - {`נוצר/נערך: ${!!placeTextHe ? placeTextHe : ""} ${!!dateTextHe ? dateTextHe : ""}`} +
: null } @@ -188,25 +213,21 @@ class AboutBox extends Component { : null ); const versionSectionEn = - (!!translationVersions?.length ? + (!!translationVersion?.versionTitle ?

- +

- { - translationVersions.map((ve) => ( - - )) - } +
: null ); const alternateSectionHe = (Object.values(this.state.versionLangMap).some(array => array?.length) ? @@ -243,6 +264,9 @@ AboutBox.propTypes = { masterPanelLanguage: PropTypes.oneOf(["english", "hebrew", "bilingual"]), title: PropTypes.string.isRequired, srefs: PropTypes.array.isRequired, + vFilter: PropTypes.array, + onRangeClick: PropTypes.func, + onCitationClick: PropTypes.func, }; diff --git a/static/js/AddToSourceSheet.jsx b/static/js/AddToSourceSheet.jsx index 3b456e1a11..a2cb3f3f5c 100644 --- a/static/js/AddToSourceSheet.jsx +++ b/static/js/AddToSourceSheet.jsx @@ -117,71 +117,103 @@ class AddToSourceSheetBox extends Component { normalize(text){ return(text.replaceAll(/()+/g, ' ').replace(/\u2009/g, ' ').replace(/<[^>]*>/g, '')); } - async addToSourceSheet() { - if (!Sefaria._uid) { - this.props.toggleSignUpModal(SignUpModalKind.AddToSheet); - } - if (!this.state.selectedSheet || !this.state.selectedSheet.id) { return; } + + async postToSheet(source) { + if (this.checkContentForImages(source.refs)) { const url = "/api/sheets/" + this.state.selectedSheet.id + "/add"; - const language = this.props.contentLanguage; - let source = {}; - if(this.props.en || this.props.he){ // legacy code to support a call to this component in Gardens. - if(this.props.srefs){ //we are saving a ref + ref's text, generally all fields should be present. - source.refs = this.props.srefs; - source.en = this.props.en; - source.he = this.props.he; - }else{ // an outside free text is being passed in. theoretically supports any interface that passes this in. In practice only legacy Gardens code. - if (this.props.en && this.props.he) { - source.outsideBiText = {he: this.props.he, en: this.props.en}; - } else { - source.outsideText = this.props.en || this.props.he; - } - } - } else if (this.props.srefs) { //regular use - this is currently the case when the component is loaded in the sidepanel or in the modal component via profiles and notes pages - source.refs = this.props.srefs; + let postData = {source: JSON.stringify(source)}; + if (this.props.note) { + postData.note = this.props.note; + } + await $.post(url, postData, this.confirmAdd); + } + } + makeSourceForEden() { + if (this.props.srefs) { //we are saving a ref + ref's text, generally all fields should be present. + source.refs = this.props.srefs; + source.en = this.props.en; + source.he = this.props.he; + } else { // an outside free text is being passed in. theoretically supports any interface that passes this in. In practice only legacy Gardens code. + if (this.props.en && this.props.he) { + source.outsideBiText = {he: this.props.he, en: this.props.en}; + } else { + source.outsideText = this.props.en || this.props.he; + } + } + } + async handleSelectedWords(source, lan) { + // If something is highlighted and main panel language is not bilingual: + // Use passed in language to determine which version this highlight covers. + let selectedWords = this.props.selectedWords; //if there was highlighted single panel + const language = this.props.contentLanguage; + if (!selectedWords || language === "bilingual") { + return; + } + let segments = await sheetsUtils.getSegmentObjs(source.refs); + selectedWords = this.normalize(selectedWords); + segments = segments.map(segment => ({ + ...segment, + [lan]: this.normalize(segment[lan]) + })); + for (let iSegment = 0; iSegment < segments.length; iSegment++) { + const segment = segments[iSegment]; + if (iSegment === 0){ + let criticalIndex = this.longestSuffixPrefixIndex(segment[lan], selectedWords); + const ellipse = criticalIndex === 0 ? "" : "..."; + segment[lan] = ellipse + segment[lan].slice(criticalIndex); + } + else if (iSegment == segments.length-1){ + let criticalIndex = this.longestPrefixSuffixIndex(segment[lan], selectedWords); + const ellipse = criticalIndex === segment[lan].length-1 ? "" : "..."; + const chunk = segment[lan].slice(0, criticalIndex) + segment[lan] = chunk + ellipse; + } + } + source[lan] = sheetsUtils.segmentsToSourceText(segments, lan); + } + + async handleSameDirectionVersions() { + for (const lang of ['he', 'en']) { + const version = this.props.currObjectVersions[lang]; + const source = { + refs: this.props.srefs, + [`version-${version.language}`]: version.versionTitle + } + await this.postToSheet(source); + } + } - const { en, he } = this.props.currVersions ? this.props.currVersions : {"en": null, "he": null}; //the text we are adding may be non-default version - if (he) { source["version-he"] = he; } - if (en) { source["version-en"] = en; } + async addToSourceSheet() { + if (!Sefaria._uid) { + this.props.toggleSignUpModal(SignUpModalKind.AddToSheet); + } + if (!this.state.selectedSheet || !this.state.selectedSheet.id) { + return; + } - // If something is highlighted and main panel language is not bilingual: - // Use passed in language to determine which version this highlight covers. - let selectedWords = this.props.selectedWords; //if there was highlighted single panel - if (selectedWords && language != "bilingual") { - let lan = language.slice(0,2); - let segments = await sheetsUtils.getSegmentObjs(source.refs); - selectedWords = this.normalize(selectedWords); - segments = segments.map(segment => ({ - ...segment, - [lan]: this.normalize(segment[lan]) - })); - for (let iSegment = 0; iSegment < segments.length; iSegment++) { - const segment = segments[iSegment]; - if (iSegment == 0){ - let criticalIndex = this.longestSuffixPrefixIndex(segment[lan], selectedWords); - const ellipse = criticalIndex == 0 ? "" : "..."; - segment[lan] = ellipse + segment[lan].slice(criticalIndex); - } - else if (iSegment == segments.length-1){ - let criticalIndex = this.longestPrefixSuffixIndex(segment[lan], selectedWords); - const ellipse = criticalIndex == segment[lan].length-1 ? "" : "..."; - const chunk = segment[lan].slice(0, criticalIndex) - segment[lan] = chunk + ellipse; - } - } + const source = {}; + let en, he; + if (this.props.en || this.props.he) { // legacy code to support a call to this component in Gardens. + this.makeSourceForEden(); + } else if (this.props.srefs) { //regular use - this is currently the case when the component is loaded in the sidepanel or in the modal component via profiles and notes pages + source.refs = this.props.srefs; - source[lan] = sheetsUtils.segmentsToSourceText(segments, lan); - } - } - if (this.checkContentForImages(source.refs)) { - let postData = {source: JSON.stringify(source)}; - if (this.props.note) { - postData.note = this.props.note; - } - $.post(url, postData, this.confirmAdd); + ({ en, he } = this.props.currObjectVersions || {"en": null, "he": null}); //the text we are adding may be non-default version + if (en?.direction && en?.direction === he?.direction) { + await this.handleSameDirectionVersions(); + return; + } else if (en?.direction === 'rtl' || he?.direction === 'ltr') { + ([en, he] = [he, en]); } + + if (he) { source["version-he"] = he.versionTitle; } + if (en) { source["version-en"] = en.versionTitle; } + } + const contentLang = he?.language || en?.language; // this matters only if one language is shown. + await this.handleSelectedWords(source, contentLang); + await this.postToSheet(source); } checkContentForImages(refs) { // validate texts corresponding to refs have no images before posting them to sheet diff --git a/static/js/BookPage.jsx b/static/js/BookPage.jsx index 3469dbd436..e7514e0851 100644 --- a/static/js/BookPage.jsx +++ b/static/js/BookPage.jsx @@ -26,10 +26,12 @@ import ExtendedNotes from './ExtendedNotes'; import classNames from 'classnames'; import PropTypes from 'prop-types'; import Component from 'react-class'; -import {ContentLanguageContext} from './context'; +import {ReaderPanelContext} from './context'; import Hebrew from './sefaria/hebrew.js'; import ReactTags from 'react-tag-autocomplete'; +import ReaderDisplayOptionsMenu from "./ReaderDisplayOptionsMenu"; +import {DropdownMenu} from "./common/DropdownMenu"; import Cookies from "js-cookie"; @@ -67,7 +69,7 @@ class BookPage extends Component { } getData() { // Gets data about this text from cache, which may be null. - return Sefaria.text(this.getDataRef(), {context: 1, enVersion: this.props.currVersions.en, heVersion: this.props.currVersions.he}); + return Sefaria.text(this.getDataRef(), {context: 1, enVersion: this.props.currVersions.en?.versionTitle, heVersion: this.props.currVersions.he?.versionTitle}); } loadData() { // Ensures data this text is in cache, rerenders after data load if needed @@ -89,7 +91,7 @@ class BookPage extends Component { let currObjectVersions = {en: null, he: null}; for(let [lang,ver] of Object.entries(this.props.currVersions)){ if(!!ver){ - let fullVer = versions.find(version => version.versionTitle == ver && version.language == lang); + let fullVer = versions.find(version => version.versionTitle == ver.versionTitle && version.language == lang); currObjectVersions[lang] = fullVer ? fullVer : null; } } @@ -131,18 +133,15 @@ class BookPage extends Component { currentVersion.merged = !!(currentVersion.sources); return currentVersion; } - openVersion(version, language) { + openVersion(version, language, versionLanguageFamily) { // Selects a version and closes this menu to show it. // Calling this functon wihtout parameters resets to default - this.props.selectVersion(version, language); + this.props.selectVersion(version, language, versionLanguageFamily); this.props.close(); } isBookToc() { return (this.props.mode == "book toc") } - isTextToc() { - return (this.props.mode == "text toc") - } extendedNotesBack(event){ return null; } @@ -168,7 +167,7 @@ class BookPage extends Component { catUrl = "/texts/" + category; } - const readButton = !this.state.indexDetails || this.isTextToc() || this.props.compare ? null : + const readButton = !this.state.indexDetails || this.props.compare ? null : Sefaria.lastPlaceForText(title) ? Continue Reading @@ -209,28 +208,21 @@ class BookPage extends Component { return (
- {this.isTextToc() || this.props.compare ? + {this.props.compare ? <>
- {this.props.compare ? - : }
- {this.props.compare ?
{title}
- : -
- Table of Contents -
}
{Sefaria.interfaceLang !== "hebrew" ? - + )}> : }
diff --git a/static/js/ComparePanelHeader.jsx b/static/js/ComparePanelHeader.jsx index 0de8a1ed3c..9939d8de2c 100644 --- a/static/js/ComparePanelHeader.jsx +++ b/static/js/ComparePanelHeader.jsx @@ -9,8 +9,11 @@ import { SearchButton, } from './Misc'; import {ContentText} from "./ContentText"; +import {DropdownMenu} from "./common/DropdownMenu"; +import ReaderDisplayOptionsMenu from "./ReaderDisplayOptionsMenu"; +import {ReaderPanelContext} from "./context"; -const ComparePanelHeader = ({ search, category, openDisplaySettings, navHome, catTitle, heCatTitle, +const ComparePanelHeader = ({ search, category, openDisplaySettings, navHome, catTitle, heCatTitle, onBack, openSearch }) => { if (search) { @@ -34,7 +37,7 @@ const ComparePanelHeader = ({ search, category, openDisplaySettings, navHome, ca
{Sefaria.interfaceLang !== "hebrew" ? - + )}> : null} ); @@ -47,9 +50,9 @@ const ComparePanelHeader = ({ search, category, openDisplaySettings, navHome, ca - {(Sefaria.interfaceLang === "hebrew" || !openDisplaySettings) ? + {(Sefaria.interfaceLang === "hebrew") ? - : } + : )}>} ); } diff --git a/static/js/ConnectionsPanel.jsx b/static/js/ConnectionsPanel.jsx index ad23187199..868f3acffa 100644 --- a/static/js/ConnectionsPanel.jsx +++ b/static/js/ConnectionsPanel.jsx @@ -17,7 +17,8 @@ import { } from './Media'; import { CategoryFilter, TextFilter } from './ConnectionFilters'; -import React, { useRef, useState, useEffect } from 'react'; +import React, { useContext, useState, useEffect } from 'react'; +import { ReaderPanelContext } from './context'; import PropTypes from 'prop-types'; import ReactDOM from 'react-dom'; import Sefaria from './sefaria/sefaria'; @@ -48,7 +49,7 @@ class ConnectionsPanel extends Component { this.state = { flashMessage: null, currObjectVersions: { en: null, he: null }, - mainVersionLanguage: props.masterPanelLanguage === "bilingual" ? "hebrew" : props.masterPanelLanguage, + // mainVersionLanguage: props.masterPanelLanguage === "bilingual" ? "hebrew" : props.masterPanelLanguage, availableTranslations: [], linksLoaded: false, // has the list of refs been loaded connectionSummaryCollapsed: true, @@ -61,7 +62,7 @@ class ConnectionsPanel extends Component { componentDidMount() { this._isMounted = true; this.loadData(); - this.getCurrentVersions(); + this.setCurrentVersions(); this.debouncedCheckVisibleSegments = Sefaria.util.debounce(this.checkVisibleSegments, 100); this.addScrollListener(); } @@ -88,11 +89,10 @@ class ConnectionsPanel extends Component { this.props.setConnectionsMode("Resources"); } - if (prevProps.currVersions.en !== this.props.currVersions.en || - prevProps.currVersions.he !== this.props.currVersions.he || + if (!Sefaria.areBothVersionsEqual(prevProps.currVersions, this.props.currVersions) || prevProps.masterPanelLanguage !== this.props.masterPanelLanguage || prevProps.srefs[0] !== this.props.srefs[0]) { - this.getCurrentVersions(); + this.setCurrentVersions(); } if (prevProps.mode !== this.props.mode || prevProps.connectionsCategory !== this.props.connectionsCategory) { @@ -183,7 +183,7 @@ class ConnectionsPanel extends Component { return Sefaria.sectionRef(Sefaria.humanRef(this.props.srefs), true) || this.props.srefs; } loadData() { - let ref = this.sectionRef(); + let ref = this.props.srefs[0]; if (!Sefaria.related(ref)) { Sefaria.related(ref, function (data) { if (this._isMounted) { @@ -224,51 +224,29 @@ class ConnectionsPanel extends Component { this.props.setConnectionsMode("Resources"); this.flashMessage("Success! You've created a new connection."); } - getData(cb) { + async getData() { // Gets data about this text from cache, which may be null. const versionPref = Sefaria.versionPreferences.getVersionPref(this.props.srefs[0]); - return Sefaria.getText(this.props.srefs[0], { context: 1, enVersion: this.props.currVersions.en, heVersion: this.props.currVersions.he, translationLanguagePreference: this.props.translationLanguagePreference, versionPref}).then(cb); + return await Sefaria.getTextFromCurrVersions(this.props.srefs[0], this.props.currVersions, this.props.translationLanguagePreference, 1); } - getVersionFromData(d, lang) { - //d - data received from this.getData() - //language - the language of the version - //console.log(d); - const currentVersionTitle = (lang === "he") ? d.heVersionTitle : d.versionTitle; - return { - ...d.versions.find(v => v.versionTitle === currentVersionTitle && v.language === lang), - title: d.indexTitle, - heTitle: d.heIndexTitle, - sources: lang === "he" ? d.heSources : d.sources, - merged: lang === "he" ? !!d.heSources : !!d.sources, + async setCurrentVersions() { + const data = await this.getData(); + let currentLanguage = this.props.masterPanelLanguage; + if (currentLanguage === "bilingual") { + currentLanguage = "hebrew" } - } - getCurrentVersions() { - const data = this.getData((data) => { - let currentLanguage = this.props.masterPanelLanguage; - if (currentLanguage === "bilingual") { - currentLanguage = "hebrew" - } - if (!data || data.error) { - this.setState({ - currObjectVersions: { en: null, he: null }, - mainVersionLanguage: currentLanguage, - }); - return - } - if (currentLanguage === "hebrew" && !data.he.length) { - currentLanguage = "english" - } - if (currentLanguage === "english" && !data.text.length) { - currentLanguage = "hebrew" - } + if (!data || data.error) { this.setState({ - currObjectVersions: { - en: ((this.props.masterPanelLanguage !== "hebrew" && !!data.text.length) || (this.props.masterPanelLanguage === "hebrew" && !data.he.length)) ? this.getVersionFromData(data, "en") : null, - he: ((this.props.masterPanelLanguage !== "english" && !!data.he.length) || (this.props.masterPanelLanguage === "english" && !data.text.length)) ? this.getVersionFromData(data, "he") : null, - }, - mainVersionLanguage: currentLanguage, - sectionRef: data.sectionRef, + currObjectVersions: { en: null, he: null }, }); + } + const [primary, translation] = Sefaria.getPrimaryAndTranslationFromVersions(data.versions); + this.setState({ + currObjectVersions: { + en: ((this.props.masterPanelLanguage !== "hebrew" && !!data.text.length) || (this.props.masterPanelLanguage === "hebrew" && !data.he.length)) ? translation : null, + he: ((this.props.masterPanelLanguage !== "english" && !!data.he.length) || (this.props.masterPanelLanguage === "english" && !data.text.length)) ? primary : null, + }, + sectionRef: data.sectionRef, }); } checkSrefs(srefs) { @@ -400,7 +378,6 @@ class ConnectionsPanel extends Component { onCitationClick={this.props.onCitationClick} handleSheetClick={this.props.handleSheetClick} openNav={this.props.openNav} - openDisplaySettings={this.props.openDisplaySettings} closePanel={this.props.closePanel} selectedWords={this.props.selectedWords} checkVisibleSegments={this.checkVisibleSegments} @@ -417,14 +394,14 @@ class ConnectionsPanel extends Component { selectedWordsForSheet = null; } else { // add source from sheet itself refForSheet = this.props.srefs; - versionsForSheet = this.props.currVersions; + versionsForSheet = this.state.currObjectVersions; selectedWordsForSheet = this.props.selectedWords; nodeRef = this.props.nodeRef; } content = (
); - } else if (this.props.mode === "About") { + } else if (this.props.mode === "About" || this.props.mode === 'Version Open') { content = ( { - const editText = canEditText ? function () { + const {textsData} = useContext(ReaderPanelContext); + const editText = canEditText && textsData ? function () { + const {primaryLang, translationLang} = textsData; let refString = srefs[0]; let currentPath = Sefaria.util.currentPath(); - let currentLangParam; - const langCode = masterPanelLanguage.slice(0, 2); - if (currVersions[langCode]) { - refString += "/" + encodeURIComponent(langCode) + "/" + encodeURIComponent(currVersions[langCode]); + const language = (masterPanelLanguage === 'english') ? translationLang : primaryLang; + const langCode = language.slice(0, 2); + const currVersionsLangCode = masterPanelLanguage.slice(0, 2); + const {versionTitle} = currVersions[currVersionsLangCode]; + if (versionTitle) { + refString += "/" + encodeURIComponent(langCode) + "/" + encodeURIComponent(versionTitle); } let path = "/edit/" + refString; let nextParam = "?next=" + encodeURIComponent(currentPath); diff --git a/static/js/ConnectionsPanelHeader.jsx b/static/js/ConnectionsPanelHeader.jsx index 9172a818a9..39cc76ed59 100644 --- a/static/js/ConnectionsPanelHeader.jsx +++ b/static/js/ConnectionsPanelHeader.jsx @@ -1,4 +1,4 @@ -import {InterfaceText, EnglishText, HebrewText, LanguageToggleButton, CloseButton } from "./Misc"; +import {InterfaceText, EnglishText, HebrewText, LanguageToggleButton, CloseButton, DisplaySettingsButton} from "./Misc"; import {RecentFilterSet} from "./ConnectionFilters"; import React from 'react'; import ReactDOM from 'react-dom'; @@ -7,6 +7,9 @@ import Sefaria from './sefaria/sefaria'; import classNames from 'classnames'; import PropTypes from 'prop-types'; import Component from 'react-class'; +import {ReaderPanelContext} from "./context"; +import {DropdownMenu} from "./common/DropdownMenu"; +import ReaderDisplayOptionsMenu from "./ReaderDisplayOptionsMenu"; class ConnectionsPanelHeader extends Component { @@ -113,13 +116,15 @@ class ConnectionsPanelHeader extends Component { if (this.props.multiPanel) { const toggleLang = Sefaria.util.getUrlVars()["lang2"] === "en" ? "he" : "en"; const langUrl = Sefaria.util.replaceUrlParam("lang2", toggleLang); + const showOneLanguage = !Sefaria._siteSettings.TORAH_SPECIFIC || Sefaria.interfaceLang === "hebrew"; + const toggleButton = (showOneLanguage) ? null : (this.props.connectionsMode === 'TextList') ? + }> : + ; const closeUrl = Sefaria.util.removeUrlParam("with"); return (
{title}
- {Sefaria.interfaceLang !== "hebrew" && Sefaria._siteSettings.TORAH_SPECIFIC ? - - : null } + {toggleButton}
); diff --git a/static/js/ContentText.jsx b/static/js/ContentText.jsx index 2e0c14049d..41346075ad 100644 --- a/static/js/ContentText.jsx +++ b/static/js/ContentText.jsx @@ -2,6 +2,7 @@ import React from "react"; import {useContentLang} from './Hooks'; import Sefaria from './sefaria/sefaria'; import ReactMarkdown from "react-markdown"; +import PropTypes from "prop-types"; const ContentText = (props) => { /* Renders content language throughout the site (content that comes from the database and is not interface language). @@ -18,24 +19,28 @@ const ContentText = (props) => { return langAndContentItems.map(item => ); }; -const VersionContent = (props) => { - /* Used to render content of Versions. - * imageLoadCallback is called to update segment numbers placement - * overrideLanguage a string with the language name (full not 2 letter) to force to render to overriding what the content language context says. Can be useful if calling object determines one langugae is missing in a dynamic way - * defaultToInterfaceOnBilingual use if you want components not to render all languages in bilingual mode, and default them to what the interface language is - * See filterContentTextByLang for more documentation */ - const langAndContentItems = _filterContentTextByLang(props); - const [languageToFilter, _] = useContentLang(props.defaultToInterfaceOnBilingual, props.overrideLanguage); - return langAndContentItems.map((item) => { - const [lang, content] = item; - if (Sefaria.isFullSegmentImage(content)){ - return(); - } - return (); - }) +const VersionContent = ({primary, translation, imageLoadCallback}) => { + /** + * Used to render content of Versions. + * imageLoadCallback is called to update segment numbers placement + */ + const versions = {primary, translation}; + return Object.keys(versions).map((key) => { + const version = versions[key]; + const lang = (version.direction === 'rtl') ? 'he' : 'en'; + const toFilter = key === 'primary' && !!primary && !!translation; + return (Sefaria.isFullSegmentImage(version.text)) ? + () : + (); + }); +} +VersionContent.propTypes = { + primary: PropTypes.object, + translation: PropTypes.object, + imageLoadCallback: PropTypes.func, } -const VersionImageSpan = ({lang, content, languageToFilter, imageLoadCallback}) => { +const VersionImageSpan = ({lang, content, toFilter, imageLoadCallback}) => { function getImageAttribute(imgTag, attribute) { const parser = new DOMParser(); const doc = parser.parseFromString(imgTag, 'text/html'); @@ -50,9 +55,8 @@ const VersionImageSpan = ({lang, content, languageToFilter, imageLoadCallback}) const altText = getImageAttribute(content, 'alt'); const srcText = getImageAttribute(content, 'src'); content = (
{{altText}/}

{altText}

); - if (lang === 'he' && languageToFilter === "bilingual") {content = ''} - - return({content}) + if (toFilter) {content = ''} + return ({content}) }; const _filterContentTextByLang = ({text, html, markdown, overrideLanguage, defaultToInterfaceOnBilingual=false, bilingualOrder = null}) => { @@ -77,9 +81,9 @@ const _filterContentTextByLang = ({text, html, markdown, overrideLanguage, defau return langAndContentItems; } -const ContentSpan = ({lang, content, isHTML, markdown}) => { +const ContentSpan = ({lang, content, isHTML, markdown, primaryOrTranslation}) => { return isHTML ? - + : markdown ? {content} diff --git a/static/js/ExtendedNotes.jsx b/static/js/ExtendedNotes.jsx index 739385d047..c2b5ba9752 100644 --- a/static/js/ExtendedNotes.jsx +++ b/static/js/ExtendedNotes.jsx @@ -13,7 +13,7 @@ class ExtendedNotes extends Component { this.state = {'notesLanguage': Sefaria.interfaceLang, 'extendedNotes': '', 'langToggle': false}; } getVersionData(versionList){ - const versionTitle = this.props.currVersions['en'] ? this.props.currVersions['en'] : this.props.currVersions['he']; + const versionTitle = this.props.currVersions['en'] ? this.props.currVersions['en'].versionTitle : this.props.currVersions['he'].versionTitle; const thisVersion = versionList.filter(x=>x.versionTitle===versionTitle)[0]; let extendedNotes = {'english': thisVersion.extendedNotes, 'hebrew': thisVersion.extendedNotesHebrew}; diff --git a/static/js/FontSizeButton.jsx b/static/js/FontSizeButton.jsx new file mode 100644 index 0000000000..33cd1086ea --- /dev/null +++ b/static/js/FontSizeButton.jsx @@ -0,0 +1,19 @@ +import React, {useContext} from "react"; +import {InterfaceText} from "./Misc"; +import {ReaderPanelContext} from "./context"; + +function FontSizeButtons() { + const {setOption} = useContext(ReaderPanelContext); + return ( +
+ + Font Size + +
+ ); +} +export default FontSizeButtons; diff --git a/static/js/Header.jsx b/static/js/Header.jsx index 53ea388954..be88743550 100644 --- a/static/js/Header.jsx +++ b/static/js/Header.jsx @@ -40,38 +40,38 @@ const LoggedOutDropdown = () => { return encodeURIComponent(Sefaria.util.currentPath()); } return ( - }> - - - - - - - -
- Site Language -
-
- - - English - - - - עברית - - -
- - - - - - - - -
- ); + }> +
+ + + + + + + +
+ Site Language +
+
+ + + English + + + + עברית + +
+ + + + + + + +
+
+); } @@ -82,71 +82,77 @@ const LoggedInDropdown = () => { } return ( - - }> - - {Sefaria.full_name} - - - - Account Settings - - - - - -
- Site Language -
-
- - English - - · - - עברית - -
- - - - - - - - - - - -
- ); + + }> +
+ + {Sefaria.full_name} + + + + Account Settings + + + + + +
+ Site Language +
+
+ + English + + · + + עברית + +
+ + + + + + + + + + + +
+
+); } const ModuleSwitcher = () => { return ( - }> - - - - - - - - - - - - - - - - - ); + }> +
+ + + + + + + + + + + + + + + +
+
+); } class Header extends Component { constructor(props) { diff --git a/static/js/Hooks.jsx b/static/js/Hooks.jsx index 115812ee73..2cc55521b4 100644 --- a/static/js/Hooks.jsx +++ b/static/js/Hooks.jsx @@ -1,6 +1,6 @@ import React, {useState, useEffect, useMemo, useCallback, useRef, useContext} from 'react'; import $ from './sefaria/sefariaJquery'; -import {ContentLanguageContext} from "./context"; +import {ReaderPanelContext} from "./context"; import Sefaria from "./sefaria/sefaria"; @@ -8,8 +8,20 @@ function useContentLang(defaultToInterfaceOnBilingual, overrideLanguage){ /* useful for determining language for content text while taking into account ContentLanguageContent and interfaceLang * `overrideLanguage` a string with the language name (full not 2 letter) to force to render to overriding what the content language context says. Can be useful if calling object determines one langugae is missing in a dynamic way * `defaultToInterfaceOnBilingual` use if you want components not to render all languages in bilingual mode, and default them to what the interface language is*/ - const contentLanguage = useContext(ContentLanguageContext); - const languageToFilter = (defaultToInterfaceOnBilingual && contentLanguage.language === "bilingual") ? Sefaria.interfaceLang : (overrideLanguage ? overrideLanguage : contentLanguage.language); + const {language, textsData} = useContext(ReaderPanelContext); + const hasContent = !!textsData; + const shownLanguage = (language === "bilingual") ? language : (language === "english" && textsData?.text?.length) ? textsData?.translationLang : textsData?.primaryLang; //the 'hebrew' of language means source + const isContentLangAmbiguous = !['hebrew', 'english'].includes(shownLanguage); + let languageToFilter; + if (defaultToInterfaceOnBilingual && hasContent && isContentLangAmbiguous) { + languageToFilter = Sefaria.interfaceLang; + } else if (overrideLanguage) { + languageToFilter = overrideLanguage; + } else if (isContentLangAmbiguous || !hasContent) { + languageToFilter = language; + } else { + languageToFilter = shownLanguage; + } const langShort = languageToFilter.slice(0,2); return [languageToFilter, langShort]; } diff --git a/static/js/LayoutButtons.jsx b/static/js/LayoutButtons.jsx new file mode 100644 index 0000000000..e4ab052267 --- /dev/null +++ b/static/js/LayoutButtons.jsx @@ -0,0 +1,64 @@ +import {useContext} from "react"; +import {ReaderPanelContext} from "./context"; +import {layoutOptions} from "./constants"; +import {InterfaceText} from "./Misc"; +import PropTypes from "prop-types"; + +const calculateLayoutState = (language, textsData, panelMode) => { + const primaryDir = textsData?.primaryDirection; + const translationDir = textsData?.translationDirection; + return (language !== 'bilingual') ? 'mono' //one text + : (primaryDir !== translationDir || panelMode === 'Sheet') ? 'mixed' //two texts with different directions + : (primaryDir === 'rtl') ? 'bi-rtl' //two rtl texts + : 'bi-ltr'; //two ltr texts +}; + +const getPath = (layoutOption, layoutState, textsData) => { + if (layoutState === 'mixed') { + const primaryDirection = textsData?.primaryDirection || 'rtl'; //no primary is the case of sheet + const translationDirection = textsData?.translationDirection || primaryDirection.split('').reverse().join(''); //when there is an empty translation it has no direction. we will show the button as opposite layouts. + const directions = (layoutOption === 'heLeft') ? `${primaryDirection}${translationDirection}` //heLeft means primary in left + : `${translationDirection}${primaryDirection}`; + if (layoutOption !== 'stacked') { + layoutOption = 'beside'; + } + layoutOption = `${layoutOption}-${directions}`; + } + return `/static/icons/${layoutState}-${layoutOption}.svg`; +}; + +const LayoutButton = ({layoutOption, layoutState}) => { + const {language, textsData, setOption, layout} = useContext(ReaderPanelContext); + const path = getPath(layoutOption, layoutState, textsData); + const optionName = (language === 'bilingual') ? 'biLayout' : 'layout'; + return ( +