diff --git a/.gitignore b/.gitignore index af52d23..c8fbb05 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ captures/ *.apk *.aab *.keystore +node_modules/ +/package-lock.json +/package.json diff --git a/anixart-tizen/assets/fonts/product_sans_medium.ttf b/anixart-tizen/assets/fonts/product_sans_medium.ttf new file mode 100644 index 0000000..fd818d6 Binary files /dev/null and b/anixart-tizen/assets/fonts/product_sans_medium.ttf differ diff --git a/anixart-tizen/assets/fonts/proxima_nova_bold.otf b/anixart-tizen/assets/fonts/proxima_nova_bold.otf new file mode 100644 index 0000000..1ea7753 Binary files /dev/null and b/anixart-tizen/assets/fonts/proxima_nova_bold.otf differ diff --git a/anixart-tizen/assets/fonts/roboto_medium.ttf b/anixart-tizen/assets/fonts/roboto_medium.ttf new file mode 100644 index 0000000..ac0f908 Binary files /dev/null and b/anixart-tizen/assets/fonts/roboto_medium.ttf differ diff --git a/anixart-tizen/assets/fonts/roboto_medium_numbers.ttf b/anixart-tizen/assets/fonts/roboto_medium_numbers.ttf new file mode 100644 index 0000000..b61ac79 Binary files /dev/null and b/anixart-tizen/assets/fonts/roboto_medium_numbers.ttf differ diff --git a/anixart-tizen/assets/fonts/ytsans_medium.ttf b/anixart-tizen/assets/fonts/ytsans_medium.ttf new file mode 100644 index 0000000..c6c02af Binary files /dev/null and b/anixart-tizen/assets/fonts/ytsans_medium.ttf differ diff --git a/anixart-tizen/assets/icons/icon.png b/anixart-tizen/assets/icons/icon.png new file mode 100644 index 0000000..ab74d2d Binary files /dev/null and b/anixart-tizen/assets/icons/icon.png differ diff --git a/anixart-tizen/assets/icons/logo_splash.png b/anixart-tizen/assets/icons/logo_splash.png new file mode 100644 index 0000000..cca89af Binary files /dev/null and b/anixart-tizen/assets/icons/logo_splash.png differ diff --git a/anixart-tizen/assets/icons/logo_splash_dark.png b/anixart-tizen/assets/icons/logo_splash_dark.png new file mode 100644 index 0000000..33c8448 Binary files /dev/null and b/anixart-tizen/assets/icons/logo_splash_dark.png differ diff --git a/anixart-tizen/config.xml b/anixart-tizen/config.xml new file mode 100644 index 0000000..19dc22d --- /dev/null +++ b/anixart-tizen/config.xml @@ -0,0 +1,16 @@ + + + + + + + Anixart + + + + + + + + + diff --git a/anixart-tizen/index.html b/anixart-tizen/index.html new file mode 100644 index 0000000..49017e9 --- /dev/null +++ b/anixart-tizen/index.html @@ -0,0 +1,25 @@ + + + + + + Anixart + + + + +
+ + + + + + + + + + + + + + diff --git a/anixart-tizen/package.json b/anixart-tizen/package.json new file mode 100644 index 0000000..45e35b4 --- /dev/null +++ b/anixart-tizen/package.json @@ -0,0 +1,10 @@ +{ + "name": "anixart-tizen", + "version": "1.0.0", + "description": "Anixart for Samsung Tizen TV", + "scripts": { + "build": "echo 'Static project — no build step required'", + "serve": "npx http-server . -p 8080 -c-1", + "package": "zip -r anixart-tizen.wgt config.xml index.html src/ styles/ assets/ -x '*.DS_Store'" + } +} diff --git a/anixart-tizen/src/api/auth.js b/anixart-tizen/src/api/auth.js new file mode 100644 index 0000000..f6abcbe --- /dev/null +++ b/anixart-tizen/src/api/auth.js @@ -0,0 +1,34 @@ +var AuthApi = { + STATUS_OK: 0, + STATUS_INVALID_LOGIN: 2, + STATUS_INVALID_PASSWORD: 3, + + signIn: function(login, password) { + return ApiClient.post('auth/signIn', { + formData: { + login: login, + password: password + } + }).then(function(response) { + if (response.profileToken && response.profileToken.token) { + Storage.setToken(response.profileToken.token); + Storage.setTokenId(response.profileToken.id); + } + if (response.profile) { + Storage.setProfile(response.profile); + } + return response; + }); + }, + + getErrorMessage: function(status) { + switch (status) { + case this.STATUS_INVALID_LOGIN: + return 'Неверная почта или никнейм'; + case this.STATUS_INVALID_PASSWORD: + return 'Неверный пароль'; + default: + return 'Ошибка входа'; + } + } +}; diff --git a/anixart-tizen/src/api/client.js b/anixart-tizen/src/api/client.js new file mode 100644 index 0000000..1eed8a5 --- /dev/null +++ b/anixart-tizen/src/api/client.js @@ -0,0 +1,75 @@ +var ApiClient = { + BASE_URL: 'https://api-s.anixsekai.com/', + + request: function(method, endpoint, options) { + options = options || {}; + var url = this.BASE_URL + endpoint; + + if (options.token) { + url += (url.indexOf('?') === -1 ? '?' : '&') + 'token=' + encodeURIComponent(options.token); + } + + if (options.queryParams) { + for (var key in options.queryParams) { + if (options.queryParams.hasOwnProperty(key)) { + url += (url.indexOf('?') === -1 ? '?' : '&') + encodeURIComponent(key) + '=' + encodeURIComponent(options.queryParams[key]); + } + } + } + + var xhr = new XMLHttpRequest(); + xhr.open(method, url, true); + + if (options.formData) { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } else if (options.json) { + xhr.setRequestHeader('Content-Type', 'application/json'); + } + + return new Promise(function(resolve, reject) { + xhr.onload = function() { + if (xhr.status >= 200 && xhr.status < 300) { + try { + resolve(JSON.parse(xhr.responseText)); + } catch (e) { + resolve(xhr.responseText); + } + } else { + reject({ status: xhr.status, text: xhr.responseText }); + } + }; + + xhr.onerror = function() { + reject({ status: 0, text: 'Network error' }); + }; + + xhr.ontimeout = function() { + reject({ status: 0, text: 'Request timeout' }); + }; + + xhr.timeout = 15000; + + if (options.formData) { + var parts = []; + for (var key in options.formData) { + if (options.formData.hasOwnProperty(key)) { + parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(options.formData[key])); + } + } + xhr.send(parts.join('&')); + } else if (options.json) { + xhr.send(JSON.stringify(options.json)); + } else { + xhr.send(); + } + }); + }, + + post: function(endpoint, options) { + return this.request('POST', endpoint, options); + }, + + get: function(endpoint, options) { + return this.request('GET', endpoint, options); + } +}; diff --git a/anixart-tizen/src/api/discover.js b/anixart-tizen/src/api/discover.js new file mode 100644 index 0000000..e3e907e --- /dev/null +++ b/anixart-tizen/src/api/discover.js @@ -0,0 +1,28 @@ +var DiscoverApi = { + getInteresting: function() { + return ApiClient.post('discover/interesting'); + }, + + getRecommendations: function(page, previousPage, token) { + return ApiClient.post('discover/recommendations/' + page, { + token: token, + queryParams: { previous_page: previousPage } + }); + }, + + getWatching: function(page, token) { + return ApiClient.post('discover/watching/' + page, { + token: token + }); + }, + + getDiscussing: function(token) { + return ApiClient.post('discover/discussing', { + token: token + }); + }, + + getComments: function() { + return ApiClient.post('discover/comments'); + } +}; diff --git a/anixart-tizen/src/api/release.js b/anixart-tizen/src/api/release.js new file mode 100644 index 0000000..b95a726 --- /dev/null +++ b/anixart-tizen/src/api/release.js @@ -0,0 +1,20 @@ +var ReleaseApi = { + getRelease: function(releaseId, token) { + return ApiClient.post('release/' + releaseId, { + token: token + }); + }, + + getEpisodes: function(releaseId, sourceId, token) { + return ApiClient.post('release/' + releaseId + '/episode', { + token: token, + queryParams: sourceId ? { sourceId: sourceId } : undefined + }); + }, + + getSources: function(releaseId, token) { + return ApiClient.post('release/' + releaseId + '/source', { + token: token + }); + } +}; diff --git a/anixart-tizen/src/app.js b/anixart-tizen/src/app.js new file mode 100644 index 0000000..c8d1c6b --- /dev/null +++ b/anixart-tizen/src/app.js @@ -0,0 +1,75 @@ +var App = { + history: [], + currentScreen: null, + + init: function() { + document.documentElement.setAttribute('data-theme', Storage.getTheme()); + + FocusManager.init(); + + if (typeof tizen !== 'undefined') { + try { + tizen.tvinputdevice.registerKeyBatch([ + 'MediaPlay', 'MediaPause', 'MediaPlayPause', + 'MediaStop', 'MediaFastForward', 'MediaRewind' + ]); + } catch (e) {} + } + + if (Storage.isLoggedIn()) { + this.showScreen('home'); + } else { + this.showScreen('login'); + } + }, + + showScreen: function(name, params) { + if (this.currentScreen && this.currentScreen !== name) { + this.history.push({ name: this.currentScreen, params: this.currentParams }); + } + this.currentScreen = name; + this.currentParams = params; + + switch (name) { + case 'login': + LoginScreen.render(); + break; + case 'home': + HomeScreen.render(); + break; + case 'details': + DetailsScreen.render(params); + break; + } + }, + + goBack: function() { + if (this.history.length > 0) { + var prev = this.history.pop(); + this.currentScreen = prev.name; + this.currentParams = prev.params; + switch (prev.name) { + case 'login': + LoginScreen.render(); + break; + case 'home': + HomeScreen.render(); + break; + case 'details': + DetailsScreen.render(prev.params); + break; + } + } else if (this.currentScreen !== 'home' && Storage.isLoggedIn()) { + this.currentScreen = 'home'; + HomeScreen.render(); + } else { + if (typeof tizen !== 'undefined') { + tizen.application.getCurrentApplication().exit(); + } + } + } +}; + +document.addEventListener('DOMContentLoaded', function() { + App.init(); +}); diff --git a/anixart-tizen/src/components/bottom-nav.js b/anixart-tizen/src/components/bottom-nav.js new file mode 100644 index 0000000..134d070 --- /dev/null +++ b/anixart-tizen/src/components/bottom-nav.js @@ -0,0 +1,52 @@ +var BottomNav = { + tabs: [ + { id: 'home', label: 'Главная', icon: '' }, + { id: 'discover', label: 'Обзор', icon: '' }, + { id: 'bookmarks', label: 'Закладки', icon: '' }, + { id: 'feed', label: 'Лента', icon: '' }, + { id: 'profile', label: 'Профиль', icon: '' } + ], + + render: function(activeTab) { + var nav = document.createElement('div'); + nav.className = 'bottom-nav'; + + for (var i = 0; i < this.tabs.length; i++) { + var tab = this.tabs[i]; + var btn = document.createElement('button'); + btn.className = 'nav-tab' + (tab.id === activeTab ? ' active' : ''); + btn.setAttribute('data-focusable', 'true'); + btn.setAttribute('data-tab', tab.id); + + var indicator = document.createElement('div'); + indicator.className = 'nav-indicator'; + + var iconWrap = document.createElement('span'); + iconWrap.className = 'nav-icon'; + iconWrap.innerHTML = tab.icon; + indicator.appendChild(iconWrap); + btn.appendChild(indicator); + + var label = document.createElement('span'); + label.className = 'nav-label'; + label.textContent = tab.label; + btn.appendChild(label); + + (function(tabId) { + btn.addEventListener('click', function() { + BottomNav.onTabClick(tabId); + }); + })(tab.id); + + nav.appendChild(btn); + } + + return nav; + }, + + onTabClick: function(tabId) { + if (tabId === 'home') { + App.showScreen('home'); + } + } +}; diff --git a/anixart-tizen/src/navigation/focus.js b/anixart-tizen/src/navigation/focus.js new file mode 100644 index 0000000..b7c8610 --- /dev/null +++ b/anixart-tizen/src/navigation/focus.js @@ -0,0 +1,169 @@ +var FocusManager = { + currentFocused: null, + sections: [], + + init: function() { + var self = this; + document.addEventListener('keydown', function(e) { + self.handleKey(e); + }); + }, + + handleKey: function(e) { + var keyCode = e.keyCode; + switch (keyCode) { + case 37: // Left + e.preventDefault(); + this.moveFocus('left'); + break; + case 38: // Up + e.preventDefault(); + this.moveFocus('up'); + break; + case 39: // Right + e.preventDefault(); + this.moveFocus('right'); + break; + case 40: // Down + e.preventDefault(); + this.moveFocus('down'); + break; + case 13: // OK/Enter + e.preventDefault(); + this.select(); + break; + case 10009: // Back (Tizen) + case 8: // Backspace + e.preventDefault(); + if (typeof App !== 'undefined') { + App.goBack(); + } + break; + case 415: // Play + case 10252: // Play/Pause (Tizen) + break; + } + }, + + setFocus: function(el) { + if (this.currentFocused) { + this.currentFocused.classList.remove('focused'); + } + this.currentFocused = el; + if (el) { + el.classList.add('focused'); + this.scrollIntoViewSmart(el); + } + }, + + scrollIntoViewSmart: function(el) { + var scrollContainer = el.closest('.section-scroll'); + if (scrollContainer) { + var elRect = el.getBoundingClientRect(); + var contRect = scrollContainer.getBoundingClientRect(); + if (elRect.left < contRect.left || elRect.right > contRect.right) { + var scrollLeft = el.offsetLeft - scrollContainer.offsetLeft - 16; + scrollContainer.scrollTo({ left: scrollLeft, behavior: 'smooth' }); + } + } + + var mainScroll = document.getElementById('main-scroll'); + if (mainScroll) { + var elRect = el.getBoundingClientRect(); + var viewHeight = window.innerHeight; + var navHeight = 72; + if (elRect.bottom > viewHeight - navHeight || elRect.top < 0) { + var scrollTop = el.offsetTop - mainScroll.offsetTop - 100; + mainScroll.scrollTo({ top: scrollTop, behavior: 'smooth' }); + } + } + }, + + getFocusables: function(container) { + container = container || document.getElementById('app'); + if (!container) return []; + return Array.prototype.slice.call(container.querySelectorAll('[data-focusable]')); + }, + + focusFirst: function(container) { + var items = this.getFocusables(container); + if (items.length > 0) { + this.setFocus(items[0]); + } + }, + + moveFocus: function(direction) { + if (!this.currentFocused) { + this.focusFirst(); + return; + } + + var focusables = this.getFocusables(); + if (focusables.length === 0) return; + + var current = this.currentFocused; + var currentRect = current.getBoundingClientRect(); + var cx = currentRect.left + currentRect.width / 2; + var cy = currentRect.top + currentRect.height / 2; + + var best = null; + var bestScore = Infinity; + + for (var i = 0; i < focusables.length; i++) { + var el = focusables[i]; + if (el === current) continue; + if (el.offsetParent === null) continue; + + var rect = el.getBoundingClientRect(); + var ex = rect.left + rect.width / 2; + var ey = rect.top + rect.height / 2; + + var dx = ex - cx; + var dy = ey - cy; + + var valid = false; + var primary, secondary; + + switch (direction) { + case 'left': + valid = dx < -5; + primary = Math.abs(dx); + secondary = Math.abs(dy); + break; + case 'right': + valid = dx > 5; + primary = Math.abs(dx); + secondary = Math.abs(dy); + break; + case 'up': + valid = dy < -5; + primary = Math.abs(dy); + secondary = Math.abs(dx); + break; + case 'down': + valid = dy > 5; + primary = Math.abs(dy); + secondary = Math.abs(dx); + break; + } + + if (!valid) continue; + + var score = secondary * 3 + primary; + if (score < bestScore) { + bestScore = score; + best = el; + } + } + + if (best) { + this.setFocus(best); + } + }, + + select: function() { + if (this.currentFocused) { + this.currentFocused.click(); + } + } +}; diff --git a/anixart-tizen/src/screens/details.js b/anixart-tizen/src/screens/details.js new file mode 100644 index 0000000..e53f92c --- /dev/null +++ b/anixart-tizen/src/screens/details.js @@ -0,0 +1,254 @@ +var DetailsScreen = { + currentRelease: null, + + render: function(params) { + var container = document.getElementById('app'); + container.innerHTML = ''; + container.className = 'screen-details'; + + var loading = document.createElement('div'); + loading.className = 'details-loading'; + loading.id = 'details-loading'; + + var spinner = document.createElement('div'); + spinner.className = 'spinner'; + loading.appendChild(spinner); + container.appendChild(loading); + + this.loadRelease(params.releaseId); + }, + + loadRelease: function(releaseId) { + var token = Storage.getToken(); + var self = this; + + ReleaseApi.getRelease(releaseId, token).then(function(response) { + var release = response.release || response; + self.currentRelease = release; + self.renderRelease(release); + }).catch(function(err) { + var container = document.getElementById('app'); + container.innerHTML = '
Ошибка загрузки
'; + }); + }, + + renderRelease: function(release) { + var container = document.getElementById('app'); + container.innerHTML = ''; + container.className = 'screen-details'; + + var bgPoster = document.createElement('div'); + bgPoster.className = 'details-bg-poster'; + if (release.image || release.poster) { + bgPoster.style.backgroundImage = 'url(' + (release.image || release.poster) + ')'; + } + container.appendChild(bgPoster); + + var gradient = document.createElement('div'); + gradient.className = 'details-gradient'; + container.appendChild(gradient); + + var scroll = document.createElement('div'); + scroll.className = 'details-scroll'; + scroll.id = 'main-scroll'; + + var hero = document.createElement('div'); + hero.className = 'details-hero'; + + var posterCard = document.createElement('div'); + posterCard.className = 'details-poster-card'; + var posterImg = document.createElement('img'); + posterImg.className = 'details-poster'; + posterImg.src = release.image || release.poster || ''; + posterImg.alt = release.title_ru || release.title || ''; + posterCard.appendChild(posterImg); + hero.appendChild(posterCard); + + var titleBlock = document.createElement('div'); + titleBlock.className = 'details-title-block'; + + var title = document.createElement('h1'); + title.className = 'details-title'; + title.textContent = release.title_ru || release.title || ''; + titleBlock.appendChild(title); + + if (release.title_en || release.title_original) { + var enTitle = document.createElement('div'); + enTitle.className = 'details-title-en'; + enTitle.textContent = release.title_en || release.title_original || ''; + titleBlock.appendChild(enTitle); + } + + var badges = document.createElement('div'); + badges.className = 'details-badges'; + + if (release.year) { + var yearBadge = document.createElement('span'); + yearBadge.className = 'badge'; + yearBadge.textContent = release.year; + badges.appendChild(yearBadge); + } + + if (release.age_rating) { + var ageBadge = document.createElement('span'); + ageBadge.className = 'badge badge-age'; + ageBadge.textContent = release.age_rating + '+'; + badges.appendChild(ageBadge); + } + + if (release.status) { + var statuses = { 1: 'Онгоинг', 2: 'Вышел', 3: 'Анонс' }; + if (statuses[release.status]) { + var statusBadge = document.createElement('span'); + statusBadge.className = 'badge'; + statusBadge.textContent = statuses[release.status]; + badges.appendChild(statusBadge); + } + } + + titleBlock.appendChild(badges); + hero.appendChild(titleBlock); + scroll.appendChild(hero); + + var actions = document.createElement('div'); + actions.className = 'details-actions'; + + var watchBtn = document.createElement('button'); + watchBtn.className = 'details-watch-btn'; + watchBtn.setAttribute('data-focusable', 'true'); + watchBtn.innerHTML = ' Смотреть'; + watchBtn.addEventListener('click', function() { + DetailsScreen.loadEpisodes(release.id); + }); + actions.appendChild(watchBtn); + + var favBtn = document.createElement('button'); + favBtn.className = 'details-action-btn'; + favBtn.setAttribute('data-focusable', 'true'); + favBtn.innerHTML = 'Закладка'; + actions.appendChild(favBtn); + + var shareBtn = document.createElement('button'); + shareBtn.className = 'details-action-btn'; + shareBtn.setAttribute('data-focusable', 'true'); + shareBtn.innerHTML = 'Поделиться'; + actions.appendChild(shareBtn); + + scroll.appendChild(actions); + + if (release.description) { + var descSection = document.createElement('div'); + descSection.className = 'details-section'; + var descTitle = document.createElement('div'); + descTitle.className = 'details-section-title'; + descTitle.textContent = 'Описание'; + descSection.appendChild(descTitle); + var descText = document.createElement('div'); + descText.className = 'details-description'; + descText.textContent = release.description; + descSection.appendChild(descText); + scroll.appendChild(descSection); + } + + if (release.genres && release.genres.length > 0) { + var genresSection = document.createElement('div'); + genresSection.className = 'details-section'; + var genresTitle = document.createElement('div'); + genresTitle.className = 'details-section-title'; + genresTitle.textContent = 'Жанры'; + genresSection.appendChild(genresTitle); + var genresList = document.createElement('div'); + genresList.className = 'details-genres'; + for (var i = 0; i < release.genres.length; i++) { + var chip = document.createElement('span'); + chip.className = 'genre-chip'; + chip.textContent = release.genres[i].name || release.genres[i]; + genresList.appendChild(chip); + } + genresSection.appendChild(genresList); + scroll.appendChild(genresSection); + } + + var episodesSection = document.createElement('div'); + episodesSection.className = 'details-section'; + episodesSection.id = 'episodes-section'; + var epTitle = document.createElement('div'); + epTitle.className = 'details-section-title'; + epTitle.textContent = 'Эпизоды'; + episodesSection.appendChild(epTitle); + var epContainer = document.createElement('div'); + epContainer.id = 'episodes-container'; + epContainer.className = 'episodes-container'; + var epLoading = document.createElement('div'); + epLoading.className = 'episodes-loading'; + epLoading.textContent = 'Загрузка...'; + epContainer.appendChild(epLoading); + episodesSection.appendChild(epContainer); + scroll.appendChild(episodesSection); + + container.appendChild(scroll); + + this.loadEpisodesList(release.id); + + setTimeout(function() { + FocusManager.setFocus(watchBtn); + }, 200); + }, + + loadEpisodesList: function(releaseId) { + var token = Storage.getToken(); + var container = document.getElementById('episodes-container'); + + ReleaseApi.getSources(releaseId, token).then(function(response) { + var sources = response.content || response || []; + if (sources.length > 0) { + return ReleaseApi.getEpisodes(releaseId, sources[0].id, token); + } + return ReleaseApi.getEpisodes(releaseId, null, token); + }).then(function(response) { + var episodes = response.content || response || []; + if (!container) return; + container.innerHTML = ''; + + if (episodes.length === 0) { + container.innerHTML = '
Нет доступных эпизодов
'; + return; + } + + var list = document.createElement('div'); + list.className = 'episodes-list section-scroll'; + + for (var i = 0; i < episodes.length; i++) { + var ep = episodes[i]; + var epCard = document.createElement('div'); + epCard.className = 'episode-card'; + epCard.setAttribute('data-focusable', 'true'); + + var epNum = document.createElement('div'); + epNum.className = 'episode-number'; + epNum.textContent = (ep.position != null ? ep.position : (i + 1)); + epCard.appendChild(epNum); + + var epName = document.createElement('div'); + epName.className = 'episode-name'; + epName.textContent = ep.name || ('Эпизод ' + (ep.position != null ? ep.position : (i + 1))); + epCard.appendChild(epName); + + list.appendChild(epCard); + } + + container.appendChild(list); + }).catch(function(err) { + if (container) { + container.innerHTML = '
Ошибка загрузки эпизодов
'; + } + }); + }, + + loadEpisodes: function(releaseId) { + var section = document.getElementById('episodes-section'); + if (section) { + section.scrollIntoView({ behavior: 'smooth' }); + } + } +}; diff --git a/anixart-tizen/src/screens/home.js b/anixart-tizen/src/screens/home.js new file mode 100644 index 0000000..579edb9 --- /dev/null +++ b/anixart-tizen/src/screens/home.js @@ -0,0 +1,360 @@ +var HomeScreen = { + render: function() { + var container = document.getElementById('app'); + container.innerHTML = ''; + container.className = 'screen-home'; + + var toolbar = this.createToolbar(); + container.appendChild(toolbar); + + var mainScroll = document.createElement('div'); + mainScroll.id = 'main-scroll'; + mainScroll.className = 'main-scroll'; + + var content = document.createElement('div'); + content.className = 'home-content'; + content.id = 'home-content'; + + var skeleton = this.createSkeleton(); + content.appendChild(skeleton); + + mainScroll.appendChild(content); + container.appendChild(mainScroll); + + var bottomNav = BottomNav.render('home'); + container.appendChild(bottomNav); + + this.loadData(); + }, + + createToolbar: function() { + var toolbar = document.createElement('div'); + toolbar.className = 'toolbar'; + + var searchBar = document.createElement('div'); + searchBar.className = 'search-bar'; + searchBar.setAttribute('data-focusable', 'true'); + + var searchIcon = document.createElement('span'); + searchIcon.className = 'search-icon'; + searchIcon.innerHTML = ''; + searchBar.appendChild(searchIcon); + + var searchText = document.createElement('span'); + searchText.className = 'search-text'; + searchText.textContent = 'Поиск аниме'; + searchBar.appendChild(searchText); + + toolbar.appendChild(searchBar); + + var actions = document.createElement('div'); + actions.className = 'toolbar-actions'; + + var settingsBtn = document.createElement('button'); + settingsBtn.className = 'toolbar-btn'; + settingsBtn.setAttribute('data-focusable', 'true'); + settingsBtn.innerHTML = ''; + actions.appendChild(settingsBtn); + + var notifBtn = document.createElement('button'); + notifBtn.className = 'toolbar-btn'; + notifBtn.setAttribute('data-focusable', 'true'); + notifBtn.innerHTML = ''; + actions.appendChild(notifBtn); + + toolbar.appendChild(actions); + return toolbar; + }, + + createSkeleton: function() { + var skeleton = document.createElement('div'); + skeleton.className = 'skeleton-container'; + skeleton.id = 'skeleton'; + + for (var s = 0; s < 3; s++) { + var section = document.createElement('div'); + section.className = 'skeleton-section'; + + var header = document.createElement('div'); + header.className = 'skeleton-header shimmer'; + section.appendChild(header); + + var row = document.createElement('div'); + row.className = 'skeleton-row'; + for (var i = 0; i < 6; i++) { + var card = document.createElement('div'); + card.className = 'skeleton-card'; + var poster = document.createElement('div'); + poster.className = 'skeleton-poster shimmer'; + card.appendChild(poster); + var line = document.createElement('div'); + line.className = 'skeleton-line shimmer'; + card.appendChild(line); + row.appendChild(card); + } + section.appendChild(row); + skeleton.appendChild(section); + } + return skeleton; + }, + + loadData: function() { + var token = Storage.getToken(); + var content = document.getElementById('home-content'); + var skeleton = document.getElementById('skeleton'); + + var promises = [ + DiscoverApi.getInteresting().catch(function() { return null; }) + ]; + + if (token) { + promises.push( + DiscoverApi.getRecommendations(0, 0, token).catch(function() { return null; }), + DiscoverApi.getWatching(0, token).catch(function() { return null; }), + DiscoverApi.getDiscussing(token).catch(function() { return null; }) + ); + } + + Promise.all(promises).then(function(results) { + if (skeleton) skeleton.remove(); + + var interesting = results[0]; + if (interesting && interesting.content && interesting.content.length > 0) { + var interestingSection = HomeScreen.createInterestingSection(interesting.content); + content.appendChild(interestingSection); + } + + if (token) { + var recommendations = results[1]; + if (recommendations && recommendations.content && recommendations.content.length > 0) { + var recSection = HomeScreen.createReleaseSection( + 'Рекомендации', + 'На основе ваших оценок', + recommendations.content, + true + ); + content.appendChild(recSection); + } + + var watching = results[2]; + if (watching && watching.content && watching.content.length > 0) { + var watchSection = HomeScreen.createReleaseSection( + 'Смотрят сейчас', + null, + watching.content, + true + ); + content.appendChild(watchSection); + } + + var discussing = results[3]; + if (discussing && discussing.content && discussing.content.length > 0) { + var discSection = HomeScreen.createReleaseSection( + 'Обсуждаемое сегодня', + null, + discussing.content, + false + ); + content.appendChild(discSection); + } + } + + setTimeout(function() { + FocusManager.focusFirst(content); + }, 100); + + }).catch(function(err) { + if (skeleton) skeleton.remove(); + var errorEl = document.createElement('div'); + errorEl.className = 'error-state'; + errorEl.textContent = 'Ошибка загрузки. Нажмите OK для повтора.'; + errorEl.setAttribute('data-focusable', 'true'); + errorEl.addEventListener('click', function() { + content.innerHTML = ''; + var newSkeleton = HomeScreen.createSkeleton(); + content.appendChild(newSkeleton); + HomeScreen.loadData(); + }); + content.appendChild(errorEl); + }); + }, + + createInterestingSection: function(items) { + var section = document.createElement('div'); + section.className = 'home-section interesting-section'; + + var scroll = document.createElement('div'); + scroll.className = 'section-scroll interesting-scroll'; + + for (var i = 0; i < items.length; i++) { + var item = items[i]; + var card = this.createInterestingCard(item); + scroll.appendChild(card); + } + + section.appendChild(scroll); + return section; + }, + + createInterestingCard: function(item) { + var release = item.release || item; + var card = document.createElement('div'); + card.className = 'interesting-card'; + card.setAttribute('data-focusable', 'true'); + card.setAttribute('data-release-id', release.id || ''); + + var img = document.createElement('img'); + img.className = 'interesting-poster'; + img.alt = release.title_ru || release.title || ''; + img.loading = 'lazy'; + if (release.image) { + img.src = release.image; + } else if (release.poster) { + img.src = release.poster; + } + img.onerror = function() { this.style.display = 'none'; }; + card.appendChild(img); + + var overlay = document.createElement('div'); + overlay.className = 'interesting-overlay'; + + var title = document.createElement('div'); + title.className = 'interesting-title'; + title.textContent = release.title_ru || release.title || ''; + overlay.appendChild(title); + + card.appendChild(overlay); + + card.addEventListener('click', function() { + App.showScreen('details', { releaseId: release.id }); + }); + + return card; + }, + + createReleaseSection: function(titleText, subtitleText, items, showMore) { + var section = document.createElement('div'); + section.className = 'home-section'; + + var header = document.createElement('div'); + header.className = 'section-header'; + + var headerLeft = document.createElement('div'); + headerLeft.className = 'section-header-left'; + + var title = document.createElement('span'); + title.className = 'section-title'; + title.textContent = titleText; + headerLeft.appendChild(title); + + if (subtitleText) { + var subtitle = document.createElement('span'); + subtitle.className = 'section-subtitle'; + subtitle.textContent = subtitleText; + headerLeft.appendChild(subtitle); + } + + header.appendChild(headerLeft); + + if (showMore) { + var moreBtn = document.createElement('span'); + moreBtn.className = 'section-show-more'; + moreBtn.textContent = 'Показать все'; + moreBtn.setAttribute('data-focusable', 'true'); + header.appendChild(moreBtn); + } + + section.appendChild(header); + + var scroll = document.createElement('div'); + scroll.className = 'section-scroll'; + + for (var i = 0; i < items.length; i++) { + var card = this.createReleaseCard(items[i]); + scroll.appendChild(card); + } + + section.appendChild(scroll); + + var separator = document.createElement('div'); + separator.className = 'section-separator'; + section.appendChild(separator); + + return section; + }, + + createReleaseCard: function(release) { + var card = document.createElement('div'); + card.className = 'release-card'; + card.setAttribute('data-focusable', 'true'); + card.setAttribute('data-release-id', release.id || ''); + + var posterWrap = document.createElement('div'); + posterWrap.className = 'release-poster-wrap'; + + var img = document.createElement('img'); + img.className = 'release-poster'; + img.alt = release.title_ru || release.title || ''; + img.loading = 'lazy'; + if (release.image) { + img.src = release.image; + } else if (release.poster) { + img.src = release.poster; + } + img.onerror = function() { + this.style.background = 'var(--color-surface)'; + }; + posterWrap.appendChild(img); + + if (release.status) { + var statusBubble = document.createElement('div'); + statusBubble.className = 'release-status'; + var statusTexts = { 1: 'Онгоинг', 2: 'Вышел', 3: 'Анонс' }; + statusBubble.textContent = statusTexts[release.status] || ''; + if (statusBubble.textContent) { + posterWrap.appendChild(statusBubble); + } + } + + card.appendChild(posterWrap); + + var info = document.createElement('div'); + info.className = 'release-info'; + + var title = document.createElement('div'); + title.className = 'release-title'; + title.textContent = release.title_ru || release.title || ''; + info.appendChild(title); + + var meta = document.createElement('div'); + meta.className = 'release-meta'; + + if (release.episodes_total || release.episodesCount) { + var eps = document.createElement('span'); + eps.textContent = (release.episodes_total || release.episodesCount || '?') + ' эп.'; + meta.appendChild(eps); + } + + if (release.grade) { + if (meta.childNodes.length > 0) { + var dot = document.createElement('span'); + dot.className = 'meta-dot'; + dot.textContent = ' · '; + meta.appendChild(dot); + } + var grade = document.createElement('span'); + grade.className = 'release-grade'; + grade.textContent = parseFloat(release.grade).toFixed(1); + meta.appendChild(grade); + } + + info.appendChild(meta); + card.appendChild(info); + + card.addEventListener('click', function() { + App.showScreen('details', { releaseId: release.id }); + }); + + return card; + } +}; diff --git a/anixart-tizen/src/screens/login.js b/anixart-tizen/src/screens/login.js new file mode 100644 index 0000000..d853597 --- /dev/null +++ b/anixart-tizen/src/screens/login.js @@ -0,0 +1,127 @@ +var LoginScreen = { + render: function() { + var container = document.getElementById('app'); + container.innerHTML = ''; + container.className = 'screen-login'; + + var wrapper = document.createElement('div'); + wrapper.className = 'login-wrapper'; + + var logo = document.createElement('div'); + logo.className = 'login-logo'; + logo.innerHTML = 'Anixart'; + wrapper.appendChild(logo); + + var title = document.createElement('h1'); + title.className = 'login-title'; + title.textContent = 'С возвращением'; + wrapper.appendChild(title); + + var subtitle = document.createElement('p'); + subtitle.className = 'login-subtitle'; + subtitle.textContent = 'Войдите, чтобы продолжить'; + wrapper.appendChild(subtitle); + + var form = document.createElement('div'); + form.className = 'login-form'; + + var loginGroup = document.createElement('div'); + loginGroup.className = 'input-group'; + var loginInput = document.createElement('input'); + loginInput.type = 'text'; + loginInput.id = 'login-input'; + loginInput.className = 'login-field'; + loginInput.placeholder = 'Почта или никнейм'; + loginInput.setAttribute('data-focusable', 'true'); + loginInput.addEventListener('focus', function() { + FocusManager.setFocus(loginInput); + }); + loginGroup.appendChild(loginInput); + form.appendChild(loginGroup); + + var passGroup = document.createElement('div'); + passGroup.className = 'input-group'; + var passInput = document.createElement('input'); + passInput.type = 'password'; + passInput.id = 'password-input'; + passInput.className = 'login-field'; + passInput.placeholder = 'Пароль'; + passInput.setAttribute('data-focusable', 'true'); + passInput.addEventListener('focus', function() { + FocusManager.setFocus(passInput); + }); + passGroup.appendChild(passInput); + + var togglePass = document.createElement('button'); + togglePass.className = 'password-toggle'; + togglePass.setAttribute('data-focusable', 'true'); + togglePass.innerHTML = ''; + togglePass.addEventListener('click', function() { + passInput.type = passInput.type === 'password' ? 'text' : 'password'; + }); + passGroup.appendChild(togglePass); + form.appendChild(passGroup); + + var errorMsg = document.createElement('div'); + errorMsg.className = 'login-error'; + errorMsg.id = 'login-error'; + form.appendChild(errorMsg); + + var loginBtn = document.createElement('button'); + loginBtn.className = 'login-button'; + loginBtn.id = 'login-button'; + loginBtn.textContent = 'Войти'; + loginBtn.setAttribute('data-focusable', 'true'); + loginBtn.addEventListener('click', function() { + LoginScreen.doLogin(); + }); + form.appendChild(loginBtn); + + wrapper.appendChild(form); + container.appendChild(wrapper); + + setTimeout(function() { + FocusManager.setFocus(loginInput); + }, 100); + }, + + doLogin: function() { + var login = document.getElementById('login-input').value.trim(); + var password = document.getElementById('password-input').value; + var errorEl = document.getElementById('login-error'); + var button = document.getElementById('login-button'); + + if (!login || !password) { + errorEl.textContent = 'Введите логин и пароль'; + errorEl.style.display = 'block'; + return; + } + + errorEl.style.display = 'none'; + button.textContent = 'Вход...'; + button.disabled = true; + + AuthApi.signIn(login, password).then(function(response) { + button.textContent = 'Войти'; + button.disabled = false; + + if (response.status && response.status !== 0) { + errorEl.textContent = AuthApi.getErrorMessage(response.status); + errorEl.style.display = 'block'; + return; + } + + if (response.profileToken && response.profileToken.token) { + App.showScreen('home'); + } else { + errorEl.textContent = 'Ошибка авторизации'; + errorEl.style.display = 'block'; + } + }).catch(function(err) { + button.textContent = 'Войти'; + button.disabled = false; + errorEl.textContent = 'Ошибка сети. Проверьте подключение.'; + errorEl.style.display = 'block'; + }); + } +}; diff --git a/anixart-tizen/src/services/storage.js b/anixart-tizen/src/services/storage.js new file mode 100644 index 0000000..0fa6b69 --- /dev/null +++ b/anixart-tizen/src/services/storage.js @@ -0,0 +1,50 @@ +var Storage = { + TOKEN_KEY: 'anixart_token', + TOKEN_ID_KEY: 'anixart_token_id', + PROFILE_KEY: 'anixart_profile', + THEME_KEY: 'anixart_theme', + + setToken: function(token) { + localStorage.setItem(this.TOKEN_KEY, token); + }, + + getToken: function() { + return localStorage.getItem(this.TOKEN_KEY); + }, + + setTokenId: function(id) { + localStorage.setItem(this.TOKEN_ID_KEY, String(id)); + }, + + getTokenId: function() { + var val = localStorage.getItem(this.TOKEN_ID_KEY); + return val ? parseInt(val, 10) : null; + }, + + setProfile: function(profile) { + localStorage.setItem(this.PROFILE_KEY, JSON.stringify(profile)); + }, + + getProfile: function() { + var val = localStorage.getItem(this.PROFILE_KEY); + return val ? JSON.parse(val) : null; + }, + + getTheme: function() { + return localStorage.getItem(this.THEME_KEY) || 'dark'; + }, + + setTheme: function(theme) { + localStorage.setItem(this.THEME_KEY, theme); + }, + + isLoggedIn: function() { + return !!this.getToken(); + }, + + clearAuth: function() { + localStorage.removeItem(this.TOKEN_KEY); + localStorage.removeItem(this.TOKEN_ID_KEY); + localStorage.removeItem(this.PROFILE_KEY); + } +}; diff --git a/anixart-tizen/styles/main.css b/anixart-tizen/styles/main.css new file mode 100644 index 0000000..d3046fd --- /dev/null +++ b/anixart-tizen/styles/main.css @@ -0,0 +1,799 @@ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + width: 100%; + height: 100%; + overflow: hidden; + background: var(--color-screen-bg); + color: var(--color-primary-text); + font-family: var(--font-body); + font-size: 16px; + -webkit-font-smoothing: antialiased; +} + +#app { + width: 100%; + height: 100%; + position: relative; + overflow: hidden; +} + +img { + display: block; +} + +button { + background: none; + border: none; + color: inherit; + cursor: pointer; + font-family: inherit; + outline: none; +} + +input { + font-family: inherit; + outline: none; +} + +.focused { + outline: var(--focus-ring-width) solid var(--focus-ring-color) !important; + outline-offset: 2px; + transform: scale(var(--focus-scale)); + transition: transform var(--focus-transition), outline var(--focus-transition); + z-index: 10; +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.shimmer { + background: linear-gradient(90deg, + var(--color-shimmer-base) 25%, + var(--color-shimmer-highlight) 50%, + var(--color-shimmer-base) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +/* ===== LOGIN SCREEN ===== */ + +.screen-login { + display: flex; + align-items: center; + justify-content: center; + background: var(--color-screen-bg); +} + +.login-wrapper { + width: 420px; + max-width: 90%; + display: flex; + flex-direction: column; + align-items: center; +} + +.login-logo { + margin-bottom: 48px; +} + +.login-logo img { + width: 120px; + height: auto; +} + +.login-title { + font-family: var(--font-display); + font-size: 28px; + font-weight: 700; + color: var(--color-primary-text); + margin-bottom: 8px; + text-align: center; +} + +.login-subtitle { + font-size: 16px; + color: var(--color-secondary-text); + margin-bottom: 40px; + text-align: center; +} + +.login-form { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; +} + +.input-group { + position: relative; + width: 100%; +} + +.login-field { + width: 100%; + height: 56px; + padding: 0 16px; + background: transparent; + border: 2px solid var(--color-border); + border-radius: 12px; + color: var(--color-primary-text); + font-size: 16px; + transition: border-color 0.2s; +} + +.login-field::placeholder { + color: var(--color-hint-text); +} + +.login-field:focus, +.login-field.focused { + border-color: var(--color-accent); +} + +.login-field.focused { + outline: none !important; + transform: none; +} + +.password-toggle { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + color: var(--color-icon-alt-tint); + border-radius: 50%; +} + +.login-error { + display: none; + color: var(--color-red); + font-size: 14px; + text-align: center; + padding: 8px; +} + +.login-button { + width: 100%; + height: 48px; + background: var(--color-button-primary); + color: var(--color-button-primary-text); + border: none; + border-radius: 12px; + font-family: var(--font-medium); + font-size: 16px; + font-weight: 500; + margin-top: 8px; + transition: opacity 0.2s; +} + +.login-button:disabled { + opacity: 0.6; +} + +.login-button.focused { + transform: scale(1.02); +} + +/* ===== TOOLBAR ===== */ + +.toolbar { + height: var(--toolbar-height); + display: flex; + align-items: center; + padding: 0 var(--section-padding-horizontal); + gap: 12px; + background: var(--color-screen-bg); + position: relative; + z-index: 20; +} + +.search-bar { + flex: 1; + height: var(--search-bar-height); + background: var(--color-search-bar-bg); + border-radius: 24px; + display: flex; + align-items: center; + padding: 0 16px; + gap: 12px; +} + +.search-icon { + color: var(--color-icon-tint); + display: flex; + align-items: center; + flex-shrink: 0; +} + +.search-text { + color: var(--color-hint-text); + font-size: 16px; +} + +.toolbar-actions { + display: flex; + gap: 4px; +} + +.toolbar-btn { + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + color: var(--color-icon-tint); + border-radius: 50%; +} + +/* ===== MAIN SCROLL ===== */ + +.main-scroll { + position: absolute; + top: var(--toolbar-height); + left: 0; + right: 0; + bottom: var(--bottom-nav-height); + overflow-y: auto; + overflow-x: hidden; + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; +} + +.main-scroll::-webkit-scrollbar { + display: none; +} + +.home-content { + padding-bottom: 24px; +} + +/* ===== SECTIONS ===== */ + +.home-section { + margin-bottom: 8px; +} + +.section-header { + height: var(--section-header-height); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--section-padding-horizontal); +} + +.section-header-left { + display: flex; + align-items: baseline; + gap: 8px; +} + +.section-title { + font-family: var(--font-medium); + font-size: var(--section-header-font-size); + color: var(--color-primary-text); + font-weight: 500; +} + +.section-subtitle { + font-size: 14px; + color: var(--color-secondary-text); +} + +.section-show-more { + font-family: var(--font-medium); + font-size: 14px; + color: var(--color-accent); + cursor: pointer; +} + +.section-scroll { + display: flex; + gap: 8px; + overflow-x: auto; + overflow-y: hidden; + padding: 0 var(--section-padding-horizontal); + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; + scroll-snap-type: x proximity; +} + +.section-scroll::-webkit-scrollbar { + display: none; +} + +.section-separator { + height: 1px; + margin: 16px var(--section-padding-horizontal) 0; + background: var(--color-separator); +} + +/* ===== INTERESTING SECTION ===== */ + +.interesting-scroll { + gap: 12px; +} + +.interesting-card { + flex-shrink: 0; + width: 320px; + height: 180px; + border-radius: 16px; + overflow: hidden; + position: relative; + background: var(--color-surface); + scroll-snap-align: start; +} + +.interesting-poster { + width: 100%; + height: 100%; + object-fit: cover; +} + +.interesting-overlay { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 12px 16px; + background: linear-gradient(transparent, rgba(0,0,0,0.7)); +} + +.interesting-title { + color: #ffffff; + font-family: var(--font-medium); + font-size: 15px; + font-weight: 500; + text-shadow: 0 1px 3px rgba(0,0,0,0.5); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ===== RELEASE CARD ===== */ + +.release-card { + flex-shrink: 0; + width: var(--release-card-min-width); + padding-top: var(--release-card-padding-top); + padding-bottom: var(--release-card-padding-bottom); + padding-left: var(--release-card-padding-start); + padding-right: var(--release-card-padding-end); + scroll-snap-align: start; +} + +.release-poster-wrap { + position: relative; + width: 100%; + border-radius: var(--release-card-corner-radius); + overflow: hidden; + background: var(--color-surface); +} + +.release-poster { + width: 100%; + height: var(--release-card-poster-height); + object-fit: cover; + display: block; +} + +.release-status { + position: absolute; + bottom: 8px; + left: 8px; + background: var(--color-status-bubble); + color: #ffffff; + font-size: 11px; + padding: 3px 8px; + border-radius: 8px; + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} + +.release-info { + margin-top: 8px; +} + +.release-title { + font-family: var(--font-medium); + font-size: var(--release-card-title-size); + font-weight: 500; + color: var(--color-primary-text); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.3; +} + +.release-meta { + display: flex; + align-items: center; + gap: 2px; + margin-top: 4px; + font-size: var(--release-card-text-size); + color: var(--color-secondary-text); +} + +.meta-dot { + color: var(--color-tertiary-text); +} + +.release-grade { + color: var(--color-secondary-text); +} + +/* ===== SKELETON ===== */ + +.skeleton-container { + padding: 0 var(--section-padding-horizontal); +} + +.skeleton-section { + margin-bottom: 24px; +} + +.skeleton-header { + width: 160px; + height: 24px; + border-radius: 8px; + margin: 16px 0; +} + +.skeleton-row { + display: flex; + gap: 8px; + overflow: hidden; +} + +.skeleton-card { + flex-shrink: 0; + width: var(--release-card-min-width); +} + +.skeleton-poster { + width: 100%; + height: var(--release-card-poster-height); + border-radius: var(--release-card-corner-radius); +} + +.skeleton-line { + width: 80%; + height: 14px; + border-radius: 4px; + margin-top: 10px; +} + +/* ===== BOTTOM NAV ===== */ + +.bottom-nav { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: var(--bottom-nav-height); + background: var(--color-bottom-nav-bg); + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + z-index: 30; + border-top: 1px solid var(--color-separator); +} + +.nav-tab { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 80px; + height: 100%; + gap: 4px; + padding: 0; +} + +.nav-indicator { + width: 56px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 14px; + transition: background 0.2s; +} + +.nav-tab.active .nav-indicator { + background: var(--color-bottom-nav-indicator); +} + +.nav-icon { + display: flex; + align-items: center; + justify-content: center; + color: var(--color-bottom-nav-icon); +} + +.nav-tab.active .nav-icon { + color: var(--color-bottom-nav-icon-active); +} + +.nav-label { + font-size: 12px; + font-family: var(--font-medium); + color: var(--color-bottom-nav-label); +} + +.nav-tab.active .nav-label { + color: var(--color-bottom-nav-label-active); +} + +/* ===== DETAILS SCREEN ===== */ + +.screen-details { + background: var(--color-screen-bg); +} + +.details-loading { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid var(--color-surface); + border-top-color: var(--color-accent); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.details-bg-poster { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 400px; + background-size: cover; + background-position: center top; + filter: blur(20px) brightness(0.5); + transform: scale(1.1); + z-index: 0; +} + +.details-gradient { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 450px; + background: linear-gradient(to bottom, transparent 0%, var(--color-screen-bg) 100%); + z-index: 1; +} + +.details-scroll { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + overflow-y: auto; + overflow-x: hidden; + z-index: 2; + padding: 40px 60px; +} + +.details-scroll::-webkit-scrollbar { + display: none; +} + +.details-hero { + display: flex; + gap: 40px; + align-items: flex-start; + margin-bottom: 32px; +} + +.details-poster-card { + flex-shrink: 0; + width: 230px; + border-radius: 24px; + overflow: hidden; + box-shadow: 0 8px 32px rgba(0,0,0,0.3); +} + +.details-poster { + width: 100%; + height: 345px; + object-fit: cover; + display: block; +} + +.details-title-block { + padding-top: 60px; +} + +.details-title { + font-family: var(--font-display); + font-size: 28px; + font-weight: 700; + color: var(--color-primary-text); + margin-bottom: 8px; + line-height: 1.2; +} + +.details-title-en { + font-size: 16px; + color: var(--color-secondary-text); + margin-bottom: 16px; +} + +.details-badges { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.badge { + background: var(--color-surface); + color: var(--color-secondary-text); + font-size: 13px; + padding: 4px 12px; + border-radius: 8px; +} + +.badge-age { + background: var(--color-accent-alpha-10); + color: var(--color-accent); +} + +.details-actions { + display: flex; + gap: 16px; + margin-bottom: 32px; + align-items: center; +} + +.details-watch-btn { + display: flex; + align-items: center; + gap: 8px; + height: 48px; + padding: 0 32px; + background: var(--color-button-primary); + color: var(--color-button-primary-text); + border: none; + border-radius: 24px; + font-family: var(--font-medium); + font-size: 16px; + font-weight: 500; +} + +.details-action-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + color: var(--color-secondary-text); + font-size: 12px; + padding: 8px 16px; + border-radius: 12px; +} + +.details-action-btn svg { + flex-shrink: 0; +} + +.details-section { + margin-bottom: 24px; +} + +.details-section-title { + font-family: var(--font-medium); + font-size: 18px; + font-weight: 500; + color: var(--color-primary-text); + margin-bottom: 12px; +} + +.details-description { + font-size: 15px; + line-height: 1.6; + color: var(--color-secondary-text); + max-width: 800px; +} + +.details-genres { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.genre-chip { + background: var(--color-surface); + color: var(--color-secondary-text); + font-size: 14px; + padding: 6px 16px; + border-radius: 16px; +} + +/* ===== EPISODES ===== */ + +.episodes-container { + min-height: 60px; +} + +.episodes-loading, +.episodes-empty { + color: var(--color-secondary-text); + font-size: 14px; + padding: 16px 0; +} + +.episodes-list { + display: flex; + gap: 8px; + overflow-x: auto; + padding-bottom: 8px; +} + +.episodes-list::-webkit-scrollbar { + display: none; +} + +.episode-card { + flex-shrink: 0; + width: 160px; + background: var(--color-surface); + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.episode-number { + font-family: var(--font-display); + font-size: 24px; + font-weight: 700; + color: var(--color-primary-text); +} + +.episode-name { + font-size: 13px; + color: var(--color-secondary-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ===== ERROR STATE ===== */ + +.error-state { + display: flex; + align-items: center; + justify-content: center; + height: 200px; + color: var(--color-secondary-text); + font-size: 16px; + text-align: center; +} diff --git a/anixart-tizen/styles/theme.css b/anixart-tizen/styles/theme.css new file mode 100644 index 0000000..34728d4 --- /dev/null +++ b/anixart-tizen/styles/theme.css @@ -0,0 +1,162 @@ +@font-face { + font-family: 'Product Sans'; + src: url('../assets/fonts/product_sans_medium.ttf') format('truetype'); + font-weight: 500; + font-display: swap; +} +@font-face { + font-family: 'Roboto Medium'; + src: url('../assets/fonts/roboto_medium.ttf') format('truetype'); + font-weight: 500; + font-display: swap; +} +@font-face { + font-family: 'Proxima Nova'; + src: url('../assets/fonts/proxima_nova_bold.otf') format('opentype'); + font-weight: 700; + font-display: swap; +} +@font-face { + font-family: 'YTSans'; + src: url('../assets/fonts/ytsans_medium.ttf') format('truetype'); + font-weight: 500; + font-display: swap; +} + +:root { + --color-accent: #f04e5c; + --color-accent-alpha-10: rgba(240, 78, 92, 0.1); + --color-accent-alpha-20: rgba(240, 78, 92, 0.2); + --color-accent-alpha-50: rgba(240, 78, 92, 0.5); + --color-accent-alpha-70: rgba(240, 78, 92, 0.7); + + --color-screen-bg: #ffffff; + --color-surface: #f5f5f5; + --color-surface-raised: #f2f2f2; + + --color-primary-text: rgba(0, 0, 0, 0.87); + --color-secondary-text: rgba(0, 0, 0, 0.54); + --color-tertiary-text: rgba(0, 0, 0, 0.38); + --color-hint-text: rgba(0, 0, 0, 0.24); + + --color-icon-tint: rgba(0, 0, 0, 0.54); + --color-icon-alt-tint: rgba(0, 0, 0, 0.38); + + --color-border: rgba(0, 0, 0, 0.1); + --color-separator: rgba(0, 0, 0, 0.05); + --color-ripple: rgba(0, 0, 0, 0.12); + + --color-bottom-nav-bg: #ffffff; + --color-bottom-nav-bg-alpha: rgba(255, 255, 255, 0.9); + --color-bottom-nav-icon: #757575; + --color-bottom-nav-icon-active: #f04e6c; + --color-bottom-nav-label: #757575; + --color-bottom-nav-label-active: #f04e6c; + --color-bottom-nav-indicator: #ffdad7; + + --color-search-bar-bg: #f5f5f5; + --color-card-bg: #ffffff; + + --color-shimmer-base: #f5f5f5; + --color-shimmer-highlight: #eeeeee; + + --color-green: #73c978; + --color-blue: #6979ce; + --color-yellow: #ffd468; + --color-red: #ff605b; + --color-purple: #c373c9; + + --color-badge-bg: #f04e5c; + --color-fab-bg: #ffdad7; + + --color-button-primary: #f04e5c; + --color-button-primary-text: #ffffff; + + --color-status-bubble: rgba(0, 0, 0, 0.45); + + --release-card-min-width: 124px; + --release-card-poster-height: 162px; + --release-card-padding-top: 12px; + --release-card-padding-bottom: 4px; + --release-card-padding-start: 8px; + --release-card-padding-end: 8px; + --release-card-title-size: 13px; + --release-card-text-size: 12px; + --release-card-corner-radius: 16px; + + --release-poster-width-regular: 104px; + --release-poster-height-regular: 156px; + --release-poster-corner-regular: 16px; + + --section-header-height: 56px; + --section-header-font-size: 18px; + --section-padding-horizontal: 16px; + + --toolbar-height: 56px; + --search-bar-height: 48px; + + --bottom-nav-height: 56px; + + --font-body: 'Roboto', 'Roboto Medium', -apple-system, sans-serif; + --font-display: 'YTSans', 'Product Sans', 'Roboto Medium', sans-serif; + --font-medium: 'Roboto Medium', 'Roboto', sans-serif; + + --focus-ring-color: rgba(240, 78, 92, 0.6); + --focus-ring-width: 3px; + --focus-scale: 1.05; + --focus-transition: 0.15s cubic-bezier(0.4, 0, 0.2, 1); +} + +[data-theme="dark"] { + --color-accent: #e0e0e0; + --color-accent-alpha-10: rgba(224, 224, 224, 0.1); + --color-accent-alpha-20: rgba(224, 224, 224, 0.2); + --color-accent-alpha-50: rgba(224, 224, 224, 0.5); + --color-accent-alpha-70: rgba(224, 224, 224, 0.7); + + --color-screen-bg: #121212; + --color-surface: #252525; + --color-surface-raised: #2c2c2c; + + --color-primary-text: rgba(255, 255, 255, 0.87); + --color-secondary-text: rgba(255, 255, 255, 0.7); + --color-tertiary-text: rgba(255, 255, 255, 0.38); + --color-hint-text: rgba(255, 255, 255, 0.24); + + --color-icon-tint: rgba(255, 255, 255, 0.7); + --color-icon-alt-tint: rgba(255, 255, 255, 0.38); + + --color-border: rgba(255, 255, 255, 0.1); + --color-separator: rgba(255, 255, 255, 0.05); + --color-ripple: rgba(255, 255, 255, 0.2); + + --color-bottom-nav-bg: #252525; + --color-bottom-nav-bg-alpha: rgba(37, 37, 37, 0.9); + --color-bottom-nav-icon: #616161; + --color-bottom-nav-icon-active: #e8def7; + --color-bottom-nav-label: #616161; + --color-bottom-nav-label-active: #e8def7; + --color-bottom-nav-indicator: #494458; + + --color-search-bar-bg: #252525; + --color-card-bg: #1e1e1e; + + --color-shimmer-base: #212121; + --color-shimmer-highlight: #292929; + + --color-green: #70a873; + --color-blue: #6974ad; + --color-yellow: #db9d39; + --color-red: #c24f4e; + --color-purple: #a770ac; + + --color-badge-bg: #494458; + --color-fab-bg: #494458; + + --color-button-primary: #f04e5c; + --color-button-primary-text: #ffffff; + + --color-status-bubble: rgba(0, 0, 0, 0.65); + + --focus-ring-color: rgba(232, 222, 247, 0.6); +}