diff --git a/.gitignore b/.gitignore index dc61fadfe..bae365060 100644 --- a/.gitignore +++ b/.gitignore @@ -10,8 +10,10 @@ /app/dist +/app/locales /app/node_modules /build/venv +/build/daemon.ver /lbry-app-venv /lbry-app /lbry-venv diff --git a/build/build.sh b/build/build.sh index c0e60e79c..92c2c5e8f 100755 --- a/build/build.sh +++ b/build/build.sh @@ -82,9 +82,17 @@ fi DAEMON_VER=$(node -e "console.log(require(\"$ROOT/app/package.json\").lbrySettings.lbrynetDaemonVersion)") DAEMON_URL_TEMPLATE=$(node -e "console.log(require(\"$ROOT/app/package.json\").lbrySettings.lbrynetDaemonUrlTemplate)") DAEMON_URL=$(echo ${DAEMON_URL_TEMPLATE//DAEMONVER/$DAEMON_VER} | sed "s/OSNAME/$OSNAME/g") -wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" -unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" -rm "$BUILD_DIR/daemon.zip" +DAEMON_VER_PATH="$BUILD_DIR/daemon.ver" +echo "$DAEMON_VER_PATH" +if [[ ! -f $DAEMON_VER_PATH || ! -f $ROOT/app/dist/lbrynet-daemon || "$(< "$DAEMON_VER_PATH")" != "$DAEMON_VER" ]]; then + wget --quiet "$DAEMON_URL" -O "$BUILD_DIR/daemon.zip" + unzip "$BUILD_DIR/daemon.zip" -d "$ROOT/app/dist/" + rm "$BUILD_DIR/daemon.zip" + echo "$DAEMON_VER" > "$DAEMON_VER_PATH" +else + echo "Already have daemon version $DAEMON_VER, skipping download" +fi + diff --git a/ui/js/actions/content.js b/ui/js/actions/content.js index 7e41ccd52..144375b6e 100644 --- a/ui/js/actions/content.js +++ b/ui/js/actions/content.js @@ -231,14 +231,15 @@ export function doStartDownload(uri, outpoint) { return function(dispatch, getState) { const state = getState(); - if (!outpoint) { throw new Error("outpoint is required to begin a download"); } + if (!outpoint) { + throw new Error("outpoint is required to begin a download"); + } const { downloadingByOutpoint = {} } = state.fileInfo; if (downloadingByOutpoint[outpoint]) return; lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => { - dispatch({ type: types.DOWNLOADING_STARTED, data: { @@ -282,29 +283,32 @@ export function doLoadVideo(uri) { }, }); - lbry.get({ uri }).then(streamInfo => { - const timeout = - streamInfo === null || - typeof streamInfo !== "object" || - streamInfo.error == "Timeout"; + lbry + .get({ uri }) + .then(streamInfo => { + const timeout = + streamInfo === null || + typeof streamInfo !== "object" || + streamInfo.error == "Timeout"; - if (timeout) { + if (timeout) { + dispatch({ + type: types.LOADING_VIDEO_FAILED, + data: { uri }, + }); + + dispatch(doOpenModal("timedOut")); + } else { + dispatch(doDownloadFile(uri, streamInfo)); + } + }) + .catch(error => { dispatch({ type: types.LOADING_VIDEO_FAILED, data: { uri }, }); - - dispatch(doOpenModal("timedOut")); - } else { - dispatch(doDownloadFile(uri, streamInfo)); - } - }).catch(error => { - dispatch({ - type: types.LOADING_VIDEO_FAILED, - data: { uri }, + dispatch(doAlertError(error)); }); - dispatch(doAlertError(error)); - }); }; } diff --git a/ui/js/actions/settings.js b/ui/js/actions/settings.js index be614c7f5..25731e135 100644 --- a/ui/js/actions/settings.js +++ b/ui/js/actions/settings.js @@ -1,6 +1,10 @@ import * as types from "constants/action_types"; import * as settings from "constants/settings"; +import batchActions from "util/batchActions"; + import lbry from "lbry"; +import fs from "fs"; +import http from "http"; const { remote } = require("electron"); const { extname } = require("path"); @@ -84,5 +88,97 @@ export function doSetTheme(name) { // update theme dispatch(doSetClientSetting(settings.THEME, theme.name)); } +} + +export function doDownloadLanguage(langFile) { + return function(dispatch, getState) { + const destinationPath = `app/locales/${langFile}`; + const language = langFile.replace(".json", ""); + const req = http.get( + { + headers: { + "Content-Type": "text/html", + }, + host: "i18n.lbry.io", + path: `/langs/${langFile}`, + }, + response => { + if (response.statusCode === 200) { + const file = fs.createWriteStream(destinationPath); + + file.on("error", errorHandler); + file.on("finish", () => { + file.close(); + + // push to our local list + dispatch({ + type: types.DOWNLOAD_LANGUAGE_SUCCEEDED, + data: { language: language }, + }); + }); + + response.pipe(file); + } else { + errorHandler(new Error("Language request failed.")); + } + } + ); + + const errorHandler = err => { + fs.unlink(destinationPath, () => {}); // Delete the file async. (But we don't check the result) + + dispatch({ + type: types.DOWNLOAD_LANGUAGE_FAILED, + data: { language }, + }); + }; + + req.setTimeout(30000, function() { + req.abort(); + }); + + req.on("error", errorHandler); + + req.end(); + }; +} + +export function doDownloadLanguages() { + return function(dispatch, getState) { + if (!fs.existsSync(app.i18n.directory)) { + fs.mkdirSync(app.i18n.directory); + } + + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + try { + const files = JSON.parse(xhr.responseText); + const actions = []; + files.forEach(file => { + actions.push(doDownloadLanguage(file)); + }); + + dispatch(batchActions(...actions)); + } catch (err) { + throw err; + } + } else { + throw new Error( + __("The list of available languages could not be retrieved.") + ); + } + } + }; + xhr.open("get", "http://i18n.lbry.io"); + xhr.send(); + }; +} + +export function doChangeLanguage(language) { + return function(dispatch, getState) { + lbry.setClientSetting(settings.LANGUAGE, language); + app.i18n.setLocale(language); }; } diff --git a/ui/js/component/fileCard/view.jsx b/ui/js/component/fileCard/view.jsx index f26eccd1d..eb29a1b2e 100644 --- a/ui/js/component/fileCard/view.jsx +++ b/ui/js/component/fileCard/view.jsx @@ -62,7 +62,8 @@ class FileCard extends React.PureComponent { ? metadata.thumbnail : null; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; - const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); + const isRewardContent = + claim && rewardedContentClaimIds.includes(claim.claim_id); let description = ""; if (isResolvingUri && !claim) { @@ -95,8 +96,9 @@ class FileCard extends React.PureComponent {
- {isRewardContent && {" "} } - {fileInfo && {" "} } + {isRewardContent && {" "}} + {fileInfo && + {" "}}
diff --git a/ui/js/component/fileTile/view.jsx b/ui/js/component/fileTile/view.jsx index 3c58d706a..c57172408 100644 --- a/ui/js/component/fileTile/view.jsx +++ b/ui/js/component/fileTile/view.jsx @@ -71,7 +71,8 @@ class FileTile extends React.PureComponent { ? metadata.thumbnail : null; const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; - const isRewardContent = claim && rewardedContentClaimIds.includes(claim.claim_id); + const isRewardContent = + claim && rewardedContentClaimIds.includes(claim.claim_id); let onClick = () => navigate("/show", { uri }); @@ -109,7 +110,7 @@ class FileTile extends React.PureComponent {
{!hidePrice ? : null} - {isRewardContent && } + {isRewardContent && }
{uri}

{title} diff --git a/ui/js/component/iconFeatured/view.jsx b/ui/js/component/iconFeatured/view.jsx index 2a0b2b54f..651d82338 100644 --- a/ui/js/component/iconFeatured/view.jsx +++ b/ui/js/component/iconFeatured/view.jsx @@ -3,10 +3,11 @@ import { Icon } from "component/common.js"; const IconFeatured = props => { return ( - - + + ); }; diff --git a/ui/js/component/userVerify/index.js b/ui/js/component/userVerify/index.js index 1d48bfbbe..043b52c75 100644 --- a/ui/js/component/userVerify/index.js +++ b/ui/js/component/userVerify/index.js @@ -21,7 +21,7 @@ const select = (state, props) => { }; const perform = dispatch => ({ - navigate: (uri) => dispatch(doNavigate(uri)), + navigate: uri => dispatch(doNavigate(uri)), verifyUserIdentity: token => dispatch(doUserIdentityVerify(token)), }); diff --git a/ui/js/component/userVerify/view.jsx b/ui/js/component/userVerify/view.jsx index ddfc18278..679052e53 100644 --- a/ui/js/component/userVerify/view.jsx +++ b/ui/js/component/userVerify/view.jsx @@ -29,20 +29,34 @@ class UserVerify extends React.PureComponent {

{__( "To ensure you are a real person, we require a valid credit or debit card." - ) + " " + __("There is no charge at all, now or in the future.") + " " } - + ) + + " " + + __("There is no charge at all, now or in the future.") + + " "} +

{errorMessage &&

{errorMessage}

} -

- {__("You can continue without this step, but you will not be eligible to earn rewards.")} +

- navigate("/discover")} button="alt" label={__("Skip Rewards")} /> +

+ {__( + "You can continue without this step, but you will not be eligible to earn rewards." + )} +

+ navigate("/discover")} + button="alt" + label={__("Skip Rewards")} + />

); } diff --git a/ui/js/component/video/view.jsx b/ui/js/component/video/view.jsx index 3157c535d..3d10520a2 100644 --- a/ui/js/component/video/view.jsx +++ b/ui/js/component/video/view.jsx @@ -16,7 +16,11 @@ class Video extends React.PureComponent { componentWillReceiveProps(nextProps) { // reset playing state upon change path action - if (!this.isMediaSame(nextProps) && this.props.fileInfo && this.state.isPlaying) { + if ( + !this.isMediaSame(nextProps) && + this.props.fileInfo && + this.state.isPlaying + ) { this.state.isPlaying = false; } } diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index 78d12d2cf..7b2a7456d 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -113,3 +113,7 @@ export const CLAIM_REWARD_SUCCESS = "CLAIM_REWARD_SUCCESS"; export const CLAIM_REWARD_FAILURE = "CLAIM_REWARD_FAILURE"; export const CLAIM_REWARD_CLEAR_ERROR = "CLAIM_REWARD_CLEAR_ERROR"; export const FETCH_REWARD_CONTENT_COMPLETED = "FETCH_REWARD_CONTENT_COMPLETED"; + +//Language +export const DOWNLOAD_LANGUAGE_SUCCEEDED = "DOWNLOAD_LANGUAGE_SUCCEEDED"; +export const DOWNLOAD_LANGUAGE_FAILED = "DOWNLOAD_LANGUAGE_FAILED"; diff --git a/ui/js/constants/languages.js b/ui/js/constants/languages.js new file mode 100644 index 000000000..f1879e904 --- /dev/null +++ b/ui/js/constants/languages.js @@ -0,0 +1,187 @@ +const LANGUAGES = { + aa: ["Afar", "Afar"], + ab: ["Abkhazian", "Аҧсуа"], + af: ["Afrikaans", "Afrikaans"], + ak: ["Akan", "Akana"], + am: ["Amharic", "አማርኛ"], + an: ["Aragonese", "Aragonés"], + ar: ["Arabic", "العربية"], + as: ["Assamese", "অসমীয়া"], + av: ["Avar", "Авар"], + ay: ["Aymara", "Aymar"], + az: ["Azerbaijani", "Azərbaycanca / آذربايجان"], + ba: ["Bashkir", "Башҡорт"], + be: ["Belarusian", "Беларуская"], + bg: ["Bulgarian", "Български"], + bh: ["Bihari", "भोजपुरी"], + bi: ["Bislama", "Bislama"], + bm: ["Bambara", "Bamanankan"], + bn: ["Bengali", "বাংলা"], + bo: ["Tibetan", "བོད་ཡིག / Bod skad"], + br: ["Breton", "Brezhoneg"], + bs: ["Bosnian", "Bosanski"], + ca: ["Catalan", "Català"], + ce: ["Chechen", "Нохчийн"], + ch: ["Chamorro", "Chamoru"], + co: ["Corsican", "Corsu"], + cr: ["Cree", "Nehiyaw"], + cs: ["Czech", "Česky"], + cu: ["Old Church Slavonic / Old Bulgarian", "словѣньскъ / slověnĭskŭ"], + cv: ["Chuvash", "Чăваш"], + cy: ["Welsh", "Cymraeg"], + da: ["Danish", "Dansk"], + de: ["German", "Deutsch"], + dv: ["Divehi", "ދިވެހިބަސް"], + dz: ["Dzongkha", "ཇོང་ཁ"], + ee: ["Ewe", "Ɛʋɛ"], + el: ["Greek", "Ελληνικά"], + en: ["English", "English"], + eo: ["Esperanto", "Esperanto"], + es: ["Spanish", "Español"], + et: ["Estonian", "Eesti"], + eu: ["Basque", "Euskara"], + fa: ["Persian", "فارسی"], + ff: ["Peul", "Fulfulde"], + fi: ["Finnish", "Suomi"], + fj: ["Fijian", "Na Vosa Vakaviti"], + fo: ["Faroese", "Føroyskt"], + fr: ["French", "Français"], + fy: ["West Frisian", "Frysk"], + ga: ["Irish", "Gaeilge"], + gd: ["Scottish Gaelic", "Gàidhlig"], + gl: ["Galician", "Galego"], + gn: ["Guarani", "Avañe'ẽ"], + gu: ["Gujarati", "ગુજરાતી"], + gv: ["Manx", "Gaelg"], + ha: ["Hausa", "هَوُسَ"], + he: ["Hebrew", "עברית"], + hi: ["Hindi", "हिन्दी"], + ho: ["Hiri Motu", "Hiri Motu"], + hr: ["Croatian", "Hrvatski"], + ht: ["Haitian", "Krèyol ayisyen"], + hu: ["Hungarian", "Magyar"], + hy: ["Armenian", "Հայերեն"], + hz: ["Herero", "Otsiherero"], + ia: ["Interlingua", "Interlingua"], + id: ["Indonesian", "Bahasa Indonesia"], + ie: ["Interlingue", "Interlingue"], + ig: ["Igbo", "Igbo"], + ii: ["Sichuan Yi", "ꆇꉙ / 四川彝语"], + ik: ["Inupiak", "Iñupiak"], + io: ["Ido", "Ido"], + is: ["Icelandic", "Íslenska"], + it: ["Italian", "Italiano"], + iu: ["Inuktitut", "ᐃᓄᒃᑎᑐᑦ"], + ja: ["Japanese", "日本語"], + jv: ["Javanese", "Basa Jawa"], + ka: ["Georgian", "ქართული"], + kg: ["Kongo", "KiKongo"], + ki: ["Kikuyu", "Gĩkũyũ"], + kj: ["Kuanyama", "Kuanyama"], + kk: ["Kazakh", "Қазақша"], + kl: ["Greenlandic", "Kalaallisut"], + km: ["Cambodian", "ភាសាខ្មែរ"], + kn: ["Kannada", "ಕನ್ನಡ"], + ko: ["Korean", "한국어"], + kr: ["Kanuri", "Kanuri"], + ks: ["Kashmiri", "कश्मीरी / كشميري"], + ku: ["Kurdish", "Kurdî / كوردی"], + kv: ["Komi", "Коми"], + kw: ["Cornish", "Kernewek"], + ky: ["Kirghiz", "Kırgızca / Кыргызча"], + la: ["Latin", "Latina"], + lb: ["Luxembourgish", "Lëtzebuergesch"], + lg: ["Ganda", "Luganda"], + li: ["Limburgian", "Limburgs"], + ln: ["Lingala", "Lingála"], + lo: ["Laotian", "ລາວ / Pha xa lao"], + lt: ["Lithuanian", "Lietuvių"], + lv: ["Latvian", "Latviešu"], + mg: ["Malagasy", "Malagasy"], + mh: ["Marshallese", "Kajin Majel / Ebon"], + mi: ["Maori", "Māori"], + mk: ["Macedonian", "Македонски"], + ml: ["Malayalam", "മലയാളം"], + mn: ["Mongolian", "Монгол"], + mo: ["Moldovan", "Moldovenească"], + mr: ["Marathi", "मराठी"], + ms: ["Malay", "Bahasa Melayu"], + mt: ["Maltese", "bil-Malti"], + my: ["Burmese", "Myanmasa"], + na: ["Nauruan", "Dorerin Naoero"], + nd: ["North Ndebele", "Sindebele"], + ne: ["Nepali", "नेपाली"], + ng: ["Ndonga", "Oshiwambo"], + nl: ["Dutch", "Nederlands"], + nn: ["Norwegian Nynorsk", "Norsk (nynorsk)"], + no: ["Norwegian", "Norsk (bokmål / riksmål)"], + nr: ["South Ndebele", "isiNdebele"], + nv: ["Navajo", "Diné bizaad"], + ny: ["Chichewa", "Chi-Chewa"], + oc: ["Occitan", "Occitan"], + oj: ["Ojibwa", "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin"], + om: ["Oromo", "Oromoo"], + or: ["Oriya", "ଓଡ଼ିଆ"], + os: ["Ossetian / Ossetic", "Иронау"], + pa: ["Panjabi / Punjabi", "ਪੰਜਾਬੀ / पंजाबी / پنجابي"], + pi: ["Pali", "Pāli / पाऴि"], + pl: ["Polish", "Polski"], + ps: ["Pashto", "پښتو"], + pt: ["Portuguese", "Português"], + qu: ["Quechua", "Runa Simi"], + rm: ["Raeto Romance", "Rumantsch"], + rn: ["Kirundi", "Kirundi"], + ro: ["Romanian", "Română"], + ru: ["Russian", "Русский"], + rw: ["Rwandi", "Kinyarwandi"], + sa: ["Sanskrit", "संस्कृतम्"], + sc: ["Sardinian", "Sardu"], + sd: ["Sindhi", "सिनधि"], + se: ["Northern Sami", "Sámegiella"], + sg: ["Sango", "Sängö"], + sh: ["Serbo-Croatian", "Srpskohrvatski / Српскохрватски"], + si: ["Sinhalese", "සිංහල"], + sk: ["Slovak", "Slovenčina"], + sl: ["Slovenian", "Slovenščina"], + sm: ["Samoan", "Gagana Samoa"], + sn: ["Shona", "chiShona"], + so: ["Somalia", "Soomaaliga"], + sq: ["Albanian", "Shqip"], + sr: ["Serbian", "Српски"], + ss: ["Swati", "SiSwati"], + st: ["Southern Sotho", "Sesotho"], + su: ["Sundanese", "Basa Sunda"], + sv: ["Swedish", "Svenska"], + sw: ["Swahili", "Kiswahili"], + ta: ["Tamil", "தமிழ்"], + te: ["Telugu", "తెలుగు"], + tg: ["Tajik", "Тоҷикӣ"], + th: ["Thai", "ไทย / Phasa Thai"], + ti: ["Tigrinya", "ትግርኛ"], + tk: ["Turkmen", "Туркмен / تركمن"], + tl: ["Tagalog / Filipino", "Tagalog"], + tn: ["Tswana", "Setswana"], + to: ["Tonga", "Lea Faka-Tonga"], + tr: ["Turkish", "Türkçe"], + ts: ["Tsonga", "Xitsonga"], + tt: ["Tatar", "Tatarça"], + tw: ["Twi", "Twi"], + ty: ["Tahitian", "Reo Mā`ohi"], + ug: ["Uyghur", "Uyƣurqə / ئۇيغۇرچە"], + uk: ["Ukrainian", "Українська"], + ur: ["Urdu", "اردو"], + uz: ["Uzbek", "Ўзбек"], + ve: ["Venda", "Tshivenḓa"], + vi: ["Vietnamese", "Tiếng Việt"], + vo: ["Volapük", "Volapük"], + wa: ["Walloon", "Walon"], + wo: ["Wolof", "Wollof"], + xh: ["Xhosa", "isiXhosa"], + yi: ["Yiddish", "ייִדיש"], + yo: ["Yoruba", "Yorùbá"], + za: ["Zhuang", "Cuengh / Tôô / 壮语"], + zh: ["Chinese", "中文"], + zu: ["Zulu", "isiZulu"], +}; + +export default LANGUAGES; diff --git a/ui/js/jsonrpc.js b/ui/js/jsonrpc.js index e9807bea7..4d1ed5bd3 100644 --- a/ui/js/jsonrpc.js +++ b/ui/js/jsonrpc.js @@ -27,7 +27,7 @@ jsonrpc.call = function( xhr.addEventListener("load", function() { var response = JSON.parse(xhr.responseText); - let error = response.error || response.result && response.result.error; + let error = response.error || (response.result && response.result.error); if (error) { if (errorCallback) { errorCallback(error); diff --git a/ui/js/main.js b/ui/js/main.js index aed81f1c8..42c40a3a2 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -7,6 +7,7 @@ import { Provider } from "react-redux"; import store from "store.js"; import SplashScreen from "component/splash"; import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; +import { doDownloadLanguages } from "actions/settings"; import { toQueryString } from "util/query_params"; import * as types from "constants/action_types"; @@ -96,6 +97,8 @@ const updateProgress = () => { const initialState = app.store.getState(); var init = function() { + app.store.dispatch(doDownloadLanguages()); + function onDaemonReady() { window.sessionStorage.setItem("loaded", "y"); //once we've made it here once per session, we don't need to show splash again app.store.dispatch(doDaemonReady()); diff --git a/ui/js/page/backup/view.jsx b/ui/js/page/backup/view.jsx index f3d930a17..6ce9de6b6 100644 --- a/ui/js/page/backup/view.jsx +++ b/ui/js/page/backup/view.jsx @@ -20,17 +20,22 @@ class BackupPage extends React.PureComponent {
-

{__("Backup Wallet")}

+

{__("Backup Your LBRY Credits")}

{__( - "Currently, there is no automatic wallet backup, but it is fairly easy to back up manually." + "Your LBRY credits are controllable by you and only you, via wallet file(s) stored locally on your computer." )}

{__( - "To backup your wallet, make a copy of the folder listed below:" + "Currently, there is no automatic wallet backup. If you lose access to these files, you will lose your credits permanently." + )} +

+

+ {__( + "However, it is fairly easy to back up manually. To backup your wallet, make a copy of the folder listed below:" )}

@@ -45,6 +50,13 @@ class BackupPage extends React.PureComponent { )}

+

+ For more details on backing up and best practices,{" "} + . +

diff --git a/ui/js/page/filePage/view.jsx b/ui/js/page/filePage/view.jsx index 3ca23aadc..8459ed01c 100644 --- a/ui/js/page/filePage/view.jsx +++ b/ui/js/page/filePage/view.jsx @@ -111,7 +111,7 @@ class FilePage extends React.PureComponent { {!fileInfo || fileInfo.written_bytes <= 0 ? - {isRewardContent && {" "} } + {isRewardContent && {" "}} : null}

{title}

diff --git a/ui/js/page/rewards/view.jsx b/ui/js/page/rewards/view.jsx index 8d97f33f6..1e7bba459 100644 --- a/ui/js/page/rewards/view.jsx +++ b/ui/js/page/rewards/view.jsx @@ -127,7 +127,9 @@ class RewardsPage extends React.PureComponent {

- {__("This application is unable to earn rewards due to an authentication failure.")} + {__( + "This application is unable to earn rewards due to an authentication failure." + )}

diff --git a/ui/js/page/settings/index.js b/ui/js/page/settings/index.js index 8516f55f4..5fe86a7a2 100644 --- a/ui/js/page/settings/index.js +++ b/ui/js/page/settings/index.js @@ -6,13 +6,21 @@ import { doSetClientSetting, doGetThemes, doSetTheme, + doChangeLanguage, } from "actions/settings"; -import { selectDaemonSettings, selectShowNsfw } from "selectors/settings"; +import { + selectDaemonSettings, + selectShowNsfw, + selectLanguages, +} from "selectors/settings"; +import { selectCurrentLanguage } from "selectors/app"; import SettingsPage from "./view"; const select = state => ({ daemonSettings: selectDaemonSettings(state), showNsfw: selectShowNsfw(state), + language: selectCurrentLanguage(state), + languages: selectLanguages(state), }); const perform = dispatch => ({ @@ -21,6 +29,7 @@ const perform = dispatch => ({ setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setTheme: name => dispatch(doSetTheme(name)), getThemes: () => dispatch(doGetThemes), + changeLanguage: newLanguage => dispatch(doChangeLanguage(newLanguage)), }); export default connect(select, perform)(SettingsPage); diff --git a/ui/js/page/settings/view.jsx b/ui/js/page/settings/view.jsx index 54dc969a0..cc030ffd8 100644 --- a/ui/js/page/settings/view.jsx +++ b/ui/js/page/settings/view.jsx @@ -110,11 +110,10 @@ class SettingsPage extends React.PureComponent { this.props.setClientSetting(settings.SHOW_NSFW, event.target.checked); } - // onLanguageChange(language) { - // lbry.setClientSetting('language', language); - // i18n.setLocale(language); - // this.setState({language: language}) - // } + onLanguageChange(e) { + this.props.changeLanguage(e.target.value); + this.forceUpdate(); + } onShowUnavailableChange(event) {} @@ -125,7 +124,7 @@ class SettingsPage extends React.PureComponent { componentDidMount() {} render() { - const { daemonSettings } = this.props; + const { daemonSettings, language, languages } = this.props; if (!daemonSettings || Object.keys(daemonSettings).length === 0) { return ( @@ -137,6 +136,28 @@ class SettingsPage extends React.PureComponent { return (
+
+
+

{__("Language")}

+
+
+
+ + + {Object.keys(languages).map(dLang => + + )} + +
+
+

{__("Download Directory")}

diff --git a/ui/js/reducers/content.js b/ui/js/reducers/content.js index a503de7f1..62784fd0f 100644 --- a/ui/js/reducers/content.js +++ b/ui/js/reducers/content.js @@ -21,10 +21,7 @@ reducers[types.FETCH_FEATURED_CONTENT_COMPLETED] = function(state, action) { }); }; -reducers[types.FETCH_REWARD_CONTENT_COMPLETED] = function( - state, - action -) { +reducers[types.FETCH_REWARD_CONTENT_COMPLETED] = function(state, action) { const { claimIds, success } = action.data; return Object.assign({}, state, { diff --git a/ui/js/reducers/settings.js b/ui/js/reducers/settings.js index 6dc59a068..14d04f450 100644 --- a/ui/js/reducers/settings.js +++ b/ui/js/reducers/settings.js @@ -1,5 +1,6 @@ import * as types from "constants/action_types"; import * as settings from "constants/settings"; +import LANGUAGES from "constants/languages"; import lbry from "lbry"; const reducers = {}; @@ -8,7 +9,9 @@ const defaultState = { showNsfw: lbry.getClientSetting("showNsfw"), theme: lbry.getClientSetting(settings.THEME), themes: lbry.getClientSetting(settings.THEMES), + language: lbry.getClientSetting("language"), }, + languages: {}, }; reducers[types.DAEMON_SETTINGS_RECEIVED] = function(state, action) { @@ -28,6 +31,28 @@ reducers[types.CLIENT_SETTING_CHANGED] = function(state, action) { }); }; +reducers[types.DOWNLOAD_LANGUAGE_SUCCEEDED] = function(state, action) { + const languages = Object.assign({}, state.languages); + const language = action.data.language; + + const langCode = language.substring(0, 2); + + if (LANGUAGES[langCode]) { + languages[language] = + LANGUAGES[langCode][0] + " (" + LANGUAGES[langCode][1] + ")"; + } else { + languages[langCode] = langCode; + } + + return Object.assign({}, state, { languages }); +}; + +reducers[types.DOWNLOAD_LANGUAGE_FAILED] = function(state, action) { + const languages = Object.assign({}, state.languages); + delete languages[action.data.language]; + return Object.assign({}, state, { languages }); +}; + export default function reducer(state = defaultState, action) { const handler = reducers[action.type]; if (handler) return handler(state, action); diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index 574494c7f..8d9f34741 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -218,6 +218,11 @@ export const selectBadgeNumber = createSelector( state => state.badgeNumber ); +export const selectCurrentLanguage = createSelector( + _selectState, + () => app.i18n.getLocale() || "en" +); + export const selectPathAfterAuth = createSelector( _selectState, state => state.pathAfterAuth diff --git a/ui/js/selectors/settings.js b/ui/js/selectors/settings.js index d875ef360..61c7dc4f0 100644 --- a/ui/js/selectors/settings.js +++ b/ui/js/selectors/settings.js @@ -21,3 +21,8 @@ export const selectShowNsfw = createSelector( selectClientSettings, clientSettings => !!clientSettings.showNsfw ); + +export const selectLanguages = createSelector( + _selectState, + state => state.languages || {} +); diff --git a/ui/js/utils.js b/ui/js/utils.js index 18e4ca21e..b41a2e0d4 100644 --- a/ui/js/utils.js +++ b/ui/js/utils.js @@ -1,5 +1,3 @@ -const { remote } = require("electron"); - /** * Thin wrapper around localStorage.getItem(). Parses JSON and returns undefined if the value * is not set yet.