From d48e63c65ebc47057095cc7cf8e0e33837f91d99 Mon Sep 17 00:00:00 2001 From: Weblate Admin Date: Fri, 15 Dec 2023 14:32:36 +0000 Subject: [PATCH 01/46] Translated using Weblate (Dutch) Currently translated at 48.5% (117 of 241 strings) Translated using Weblate (Dutch) Currently translated at 48.1% (116 of 241 strings) Co-authored-by: Weblate Admin Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/nl/ Translation: PixelDroid/pixeldroid --- app/src/main/res/values-nl/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 1f43fe1d..0e9ac476 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -142,4 +142,6 @@ %d \nVolgers + Altijd gevoelig materiaal tonen + %1$s heeft gereageerd op je bericht \ No newline at end of file From a4a2505adb76376c17b1a85882350d15e10c567b Mon Sep 17 00:00:00 2001 From: mittwerk Date: Fri, 15 Dec 2023 14:32:36 +0000 Subject: [PATCH 02/46] Translated using Weblate (Russian) Currently translated at 100.0% (241 of 241 strings) Translated using Weblate (Russian) Currently translated at 100.0% (241 of 241 strings) Translated using Weblate (Russian) Currently translated at 100.0% (241 of 241 strings) Translated using Weblate (Russian) Currently translated at 100.0% (241 of 241 strings) Translated using Weblate (Russian) Currently translated at 100.0% (241 of 241 strings) Translated using Weblate (Russian) Currently translated at 100.0% (241 of 241 strings) Translated using Weblate (Russian) Currently translated at 100.0% (241 of 241 strings) Translated using Weblate (Russian) Currently translated at 100.0% (19 of 19 strings) Translated using Weblate (Russian) Currently translated at 100.0% (241 of 241 strings) Translated using Weblate (Russian) Currently translated at 98.7% (238 of 241 strings) Co-authored-by: mittwerk Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/app/ru/ Translate-URL: https://weblate.pixeldroid.org/projects/pixeldroid/fastlane/ru/ Translation: PixelDroid/Fastlane Translation: PixelDroid/pixeldroid --- app/src/main/res/values-ru/strings.xml | 233 ++++++++++++------ .../metadata/android/ru/changelogs/12.txt | 3 + .../metadata/android/ru/changelogs/14.txt | 1 + .../metadata/android/ru/changelogs/15.txt | 1 + .../metadata/android/ru/changelogs/16.txt | 4 + .../metadata/android/ru/changelogs/17.txt | 8 + .../metadata/android/ru/changelogs/18.txt | 11 + .../metadata/android/ru/changelogs/19.txt | 9 + fastlane/metadata/android/ru/changelogs/2.txt | 7 + .../metadata/android/ru/changelogs/20.txt | 4 + .../metadata/android/ru/changelogs/23.txt | 4 + fastlane/metadata/android/ru/changelogs/3.txt | 7 + fastlane/metadata/android/ru/changelogs/4.txt | 9 + fastlane/metadata/android/ru/changelogs/5.txt | 5 + fastlane/metadata/android/ru/changelogs/7.txt | 2 + fastlane/metadata/android/ru/changelogs/8.txt | 4 + fastlane/metadata/android/ru/changelogs/9.txt | 3 + .../metadata/android/ru/full_description.txt | 4 +- 18 files changed, 245 insertions(+), 74 deletions(-) create mode 100644 fastlane/metadata/android/ru/changelogs/12.txt create mode 100644 fastlane/metadata/android/ru/changelogs/14.txt create mode 100644 fastlane/metadata/android/ru/changelogs/15.txt create mode 100644 fastlane/metadata/android/ru/changelogs/16.txt create mode 100644 fastlane/metadata/android/ru/changelogs/17.txt create mode 100644 fastlane/metadata/android/ru/changelogs/18.txt create mode 100644 fastlane/metadata/android/ru/changelogs/19.txt create mode 100644 fastlane/metadata/android/ru/changelogs/2.txt create mode 100644 fastlane/metadata/android/ru/changelogs/20.txt create mode 100644 fastlane/metadata/android/ru/changelogs/23.txt create mode 100644 fastlane/metadata/android/ru/changelogs/3.txt create mode 100644 fastlane/metadata/android/ru/changelogs/4.txt create mode 100644 fastlane/metadata/android/ru/changelogs/5.txt create mode 100644 fastlane/metadata/android/ru/changelogs/7.txt create mode 100644 fastlane/metadata/android/ru/changelogs/8.txt create mode 100644 fastlane/metadata/android/ru/changelogs/9.txt diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 334586d5..03a0bc40 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,67 +1,67 @@ - Не удалось авторизоваться - Ошибка получения токена + Не удалось пройти аутентификацию + Ошибка при получении токена Настройки Тема приложения Тема %1$s подписан(а) на вас %1$s упомянул(а) вас - %1$s поделился(ась) вашим постом - пост + %1$s поделился(-ась) вашим постом + Запостить Выйти - Что такое инстанс\? - РЕДАКТИРОВАТЬ + Что такое экземпляр? + Править Сохранить в Галерею… Загрузка… Изображение успешно загружено Переключить камеру Галерея - Нет комментариев… - Домен вашего инстанса - Подключение к Pixelfed + К этому посту нет комментариев … + Доменное имя вашего экземпляра + Подключить к Pixelfed PixelDroid Мой Профиль Настройки - Некоректный домен - Не удалось запустить браузер, есть ли он у вас\? + Недопустимый домен + Не удалось запустить браузер, есть ли у вас такой? %1$s оценил(и) ваш пост Описание… Загрузка не удалась, попробуйте ещё раз Поделиться изображением… - Вам необходимо быть в сети чтобы добавить аккаунт и использовать PixelDroid :( - CW / NSFW / Медиа для 18+ -\n(кликните что бы показать) - Не удалось зарегистрировать приложение на этом инстансе + Чтобы добавить Ваш первый аккаунт и использовать PixelDroid Вам нужно быть онлайн :( + CW / NSFW / Скрытые Медиа +\n (нажмите, чтобы показать) + Не удалось зарегистрировать приложение на этом сервере Сделать снимок Добавить другой аккаунт Pixelfed Добавить аккаунт - Не удалось получить информацию об инстансе - Комментарий + Не удалось получить информацию об экземпляре + Комментировать Комментарий: %1$s опубликован! - Ошибка комментария! + Ошибка комментирования! Поделиться изображением - Вы должны дать разрешение на запись для загрузки изображений! + Вам необходимо предоставить разрешение на запись чтобы скачивать фотографии! Вы должны дать разрешение на запись чтобы делиться фотографиями! - Комментарий не может быть пустым! + Комментарий не должен быть пустым! Опубликовано в %1$s - Нет описания + Описание отсутствует Не удалось загрузить ленту При загрузке что-то пошло не так Ошибка загрузки поста Пост успешно загружен - Не удалось загрузить пост + Загрузка поста не удалась Ошибка загрузки: некорректный формат запроса Ошибка загрузки: неверный формат изображения. Ошибка загрузки изображения! Изображение успешно сохранено - Не удалось сохранить изображение + Не могу сохранить изображение Тёмная Светлая По умолчанию (как в системе) Не удалось получить статус подписки Не удалась подписаться - Это действие недопустимо + Это действие не разрешено Не удалось отписаться Токен доступа недействителен - @@ -78,49 +78,49 @@ АККАУНТЫ ХЭШТЕГИ Не удалось отобразить кнопку подписки - Изображение, которое будет опубликовано + Публикуемое изображение Загрузка медиа завершена Повторить - Ошибка загрузки медиа, попробуйте ещё раз или проверьте состояние сети - Здесь нечего смотреть! - Открыть меню навигации - Панде грустно. Потяните, чтобы обновить. + Не удалось загрузить медиа, повторите попытку или проверьте состояние сети + Здесь не на что смотреть! + Открыть всплывающее меню + Панда недовольна. Потяните за кнопку обновления, чтобы попробовать еще раз. Что-то пошло не так… - ОБЗОР + Откройте для себя Изображение профиля Не удалось отправить жалобу - Отправлено {gmd_check_circle} + Жалоба отправлена Пожаловаться на пост @%1$s - Дополнительное сообщение для модераторов/администраторов + Необязательное сообщение для модеров/админов Поделиться ссылкой Пожаловаться Больше опций Поисковый запрос не может быть пустым - %1$s подписок - %1$s подписчиков + подписаны на %1$s + подписчики %1$s Пост %1$s О приложении - PixelDroid это свободное ПО с открытым исходным кодом, выпускаемое под лицензией GNU General Public License (версии 3 и выше) + PixelDroid - это свободное программное обеспечение с открытым исходным кодом, лицензированное в соответствии с GNU General Public License (версии 3 и выше) Сайт проекта: https://pixeldroid.org Зависимости и лицензии О PixelDroid Отписаться Добавить фото Опрос %1$s завершён - Отменить вход - OK, продолжить всё равно - Это не похоже на инстанс Pixelfed, приложение может работать с ошибками. + Отмена входа в систему + Ладно, продолжить всё равно + Похоже, это не экземпляр Pixelfed, поэтому приложение может сломаться неожиданным образом. Сохранить описание изображения Одно из изображений в посте - Невозможно получить информацию о пользователе + Не удалось получить информацию о пользователе Описание должно содержать максимум %d символ. Описание должно содержать максимум %d символа. Описание должно содержать максимум %d символов. Описание должно содержать максимум %d символов. - Изображение красной панды, талисман Pixelfed , использующей телефон - Сообщайте о проблемах или вносите свой вклад в приложение: + Это изображение красной панды, маскота Pixelfed пользующегося телефоном + Сообщите о проблемах или внесите свой вклад в работу над приложением: Помогите перевести PixelDroid на ваш язык: Язык Удалить этот пост\? @@ -155,8 +155,8 @@ %d \nПостов - Этот пост в альбоме - Предложить комментарий + Этот пост представляет собой альбом + Отправить комментарий Добавить комментарий %d комментарий @@ -165,37 +165,37 @@ %d комментариев - %d Репост - %d Репоста - %d Репостов - %d Репостов + %d Поделился(-ась) + %d Поделились + %d Поделились + %d Поделились - %d Лайк - %d Лайка - %d Лайков - %d Лайков + %d Понравилось + %d Понравилось + %d Понравилось + %d Понравилось - Добавьте описание медиа файла здесь… + Добавьте описание медиа здесь… Показывать в режиме «карусель» - Вас может смутить текстовое поле, запрашивающее доменное имя вашего \'инстанса\'. + Вас может смутить текстовое поле запрашивающее доменное имя вашего \"экземпляра\". \n -\nPixelfed это федеративная платформа и часть \"федиверса\", что означает, что она может общаться с другими платформами, говорящими на том же языке, как например Mastodon (см. https://joinmastodon.org). +\nPixelfed - это федеративная платформа и часть \"федеративной вселенной (fediverse)\", что означает, что она может общаться с другими платформами говорящими на одном языке, как например Mastodon (см. https://joinmastodon.org). \n -\nЭто также означает, что вы должны выбрать, какой сервер или \'инстанс\' Pixelfed использовать. Если вы еще ничего не знаете об этом, перейдите по ссылке: https://pixelfed.org/join +\nЭто также означает что Вам придется выбрать какой сервер или \'экземпляр\' Pixelfed использовать. Если вы еще ничего не знаете об этом, перейдите по ссылке: https://pixelfed.org/join \n -\nДополнительную информации о Pixelfed вы можете посмотреть здесь: https://pixelfed.org - Переключить в вид сетки - Отменить запрос на подписку\? - Подписаться на запрос - Вы выбрали большее изображений, чем разрешено вашим сервером (%1$s). Изображения сверх установленного лимита игнорируются. - На этом инстансе API не активирован. Свяжитесь с вашим администратором для его активации. - Не удалось удалить пост, проверить подключение\? - Ошибка при удалении поста %1$d - Здесь ничего нет :( +\nБолее подробную информацию о Pixelfed вы можете найти здесь: https://pixelfed.org. + Переключить на вид сеткой + Отменить запрос на подписку? + Запрос на подписку отправлен + Вы выбрали количество изображений превышающее позволенное вашим сервером (%1$s). Изображения сверх установленного лимита игнорируются. + API не активирован на этом экземпляре. Свяжитесь с вашим администратором для его активации. + Не удалось удалить пост, проверьте ваше подключение? + Не удалось удалить пост, ошибка %1$d + Здесь не на что смотреть :( Не удалось открыть страницу редактирования Код ошибки, возвращенный сервером: %1$d - Размер изображения в альбоме превышает максимальный размер в %1$d разрешённый инстансом (%2$d Кбайт, тогда как лимит установлен в %3$d Кбайт). По всей вероятности вы не сможете загрузить его. + Размер изображения в альбоме превышает максимальный размер в %1$d допустимый экземпляром (%2$d Кбайт, однако ограничение установлено в %3$d Кбайт). Вы не сможете загрузить его. #%1$s Файл %1$s не найден @@ -204,24 +204,113 @@ %d новых уведомлений %d новых уведомлений - Ваш сервер не поддерживает загрузку видео, возможно, вы не сможете загружать видео, включенные в этот пост - %1$s прокомментировал ваш пост + Сервер, который вы используете, не поддерживает загрузку видео, поэтому вы не сможете загрузить видео, включенное в этот пост + %1$s прокомментировал(а) ваш пост Уведомление от %1$s Новые подписчики Упоминания Поделились - Лайки + Понравилось Комментарии - Голосования + Опросы Другое %1$s, %2$s, %3$s и %4$d других %1$s, %2$s, и %3$s %1$s и %2$s Этот пост является видео Настройки уведомлений - Управляйте тем, какие уведомления вы хотите получать + Отметьте какие уведомления вы хотите получать Не удалось получить последние уведомления - Разрешение на использование камеры не предоставлено, выдайте разрешение в настройках, если хотите чтобы PixelDroid использовал камеру - Разрешение на хранилище не предоставлено, дайте разрешение в настройках, если вы хотите, чтобы PixelDroid показывал миниатюры + Разрешение на камеру не предоставлено, дайте разрешение в настройках если хотите чтобы PixelDroid использовал камеру + Разрешение на хранилище не предоставлено, дайте разрешение в настройках если хотите чтобы PixelDroid показывал миниатюры Воспроизвести видео + Всегда показывать чувствительное содержимое + Заполните описание новых постов следующим образом + Сообщение удалено из коллекции + Не удалось открыть страницу создания коллекции + Предпросмотр изображения в уведомлении об этом посте + Просмотр популярных постов за день + Что-то пошло не так. Нажмите, чтобы повторить попытку + Одно или несколько видео все еще кодируются. Дождитесь их завершения перед загрузкой на сервер + Коллекции + Популярные учетные записи + Вы уверены, что хотите удалить эту коллекцию? + Пост добавлен в коллекцию + Когда ваш аккаунт становится частным, ваши фотографии и видео на pixelfed смогут видеть только те, кого вы одобрили. На ваших существующих подписчиков это никак не повлияет. + Посты NSFW/CW по умолчанию не будут показываться в размытом виде. + + %d ответить + %d ответить + %d ответить + %d ответить + + Ошибка кодирования + Успешное кодирование! + Закодировать %1$d%% + + %d элемент успешно загружен + %d элемента успешно загружено + %d элементов успешно загружены + %d элементов успешно загружены + + Главная + Обновления + Поиск + Создать + Общество + Цветовой акцент + Выберите цветовой акцент + Выберите этот цветовой акцент + Выбранный цветовой акцент + Следующий шаг + Добавьте подробностей + Неизвестная ошибка, проверьте работает ли сервер: %1$s + Коллекция %1$s + Закладка + Удалить из закладок + Переделать + Правка этого поста позволит вам скорректировать фотографию и ее описание, но при этом будут удалены все текущие комментарии и лайки. Продолжить? + Если вы отмените эту редакцию, первоначальное сообщение больше не будет храниться на вашем аккаунте. Продолжить без повторного публикования? + Не удалось переделать пост, ошибка %1$d + Не удалось переделать пост, проверьте ваше соединение? + Не удалось (снять) закладку с поста, ошибка %1$d + Не удалось (снять) закладку, проверьте ваше соединение? + Анализ стабилизации %1$d%% + Создать новый пост + Предварительный просмотр поста + Новый пост + %1$s запрашивает подписаться на Вас + %1$s создал пост + Не удалось загрузить профиль + от%1$s + Ошибка при добавлении изображений + Заготовка описания + Исследуйте популярные учетные записи на этом экземпляре + Исследуйте популярные хэштеги на этом экземпляре + Популярные ключевые слова + Популярные посты + Посмотрите случайные посты за этот день + Вид сеткой + Вид лентой + Закладки + Удалить коллекцию + Добавить пост + Удалить пост + Выберите пост для добавления + Выберите пост для удаления + Не удалось добавить пост в коллекцию + Не удалось удалить пост из коллекции + Сохранить + Используйте динамические цвета из вашей системы + Дополнительные настройки профиля + Частная учетная запись + О себе + Ваше имя + Вы не сохранили изменения. Выйти? + Получаю ваш профиль… + Сохранение профиля + Изменения сохранены! + Измените фотографию профиля + Содержит материалы NSFW + Сменить аккаунт \ No newline at end of file diff --git a/fastlane/metadata/android/ru/changelogs/12.txt b/fastlane/metadata/android/ru/changelogs/12.txt new file mode 100644 index 00000000..6cc9c55e --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/12.txt @@ -0,0 +1,3 @@ +* Редактирование видео! Отключайте звук, обрезайте видео +* Открывайте изображения в полноэкранном режиме, масштабируйте и панорамируйте их :) +* Обновления перевода diff --git a/fastlane/metadata/android/ru/changelogs/14.txt b/fastlane/metadata/android/ru/changelogs/14.txt new file mode 100644 index 00000000..d31791c3 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/14.txt @@ -0,0 +1 @@ +Исправление для сбоев при правке, которые происходили только в режиме релиза diff --git a/fastlane/metadata/android/ru/changelogs/15.txt b/fastlane/metadata/android/ru/changelogs/15.txt new file mode 100644 index 00000000..bbf5b128 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/15.txt @@ -0,0 +1 @@ +Добавили в приложение перевод на венгерский (мадьярский) язык. Спасибо Балажу :) diff --git a/fastlane/metadata/android/ru/changelogs/16.txt b/fastlane/metadata/android/ru/changelogs/16.txt new file mode 100644 index 00000000..e13657db --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/16.txt @@ -0,0 +1,4 @@ +* Добавили proguard правила gson для устранения сбоев на экземплярах Mastodon. +* Добавление цветовой тематики с 4 различными темами +* Переход на Material 3 +* Улучшена согласованность пользовательского интерфейса diff --git a/fastlane/metadata/android/ru/changelogs/17.txt b/fastlane/metadata/android/ru/changelogs/17.txt new file mode 100644 index 00000000..c8f51f94 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/17.txt @@ -0,0 +1,8 @@ +* Улучшения в комментариях: Теперь вы можете открыть комментарий, чтобы увидеть ответы, поставить лайк, в комментариях отображается аватар автора и т. д. +* Обновления перевода. Спасибо переводчикам :). Помогите перевести PixelDroid на ваш язык на weblate.pixeldroid.org +* Безопасность: проверка зависимостей гарантирует, что зависимости, включенные в приложение, не были подделаны, PixelDroid теперь будет отказываться от любого не-HTTPS соединения +* PixelDroid теперь использует пользовательский агент "PixelDroid" вместо пользовательского агента библиотеки OkHttp. +* Все жестко закодированные строки были удалены из приложения, теперь все можно перевести. +* Некоторые улучшения в коде +* Исправлено сохранение изображений из библиотеки или общего доступа к приложению +* Исправлен еще один сбой при использовании экземпляров Mastodon diff --git a/fastlane/metadata/android/ru/changelogs/18.txt b/fastlane/metadata/android/ru/changelogs/18.txt new file mode 100644 index 00000000..29a20b29 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/18.txt @@ -0,0 +1,11 @@ +* Добавили пользовательскую графику ошибок красной панды + +* Разрешили произвольную обрезку при правке изображений + +* Улучшили метаданные F-Droid + +* Обновления перевода + +* Обновление зависимостей + +* Исправление ошибок diff --git a/fastlane/metadata/android/ru/changelogs/19.txt b/fastlane/metadata/android/ru/changelogs/19.txt new file mode 100644 index 00000000..54f42de3 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/19.txt @@ -0,0 +1,9 @@ +* Удаление метаданных фотографий перед загрузкой +* Закладки! +* Просмотр профиля в виде ленты или сетки +* Установите шаблон для ваших описаний +* Значок на значке уведомлений, если вы получили новые уведомления +* Больше функций редактирования видео: обрезка, изменение скорости, добавление стабилизации +* Реализация динамических цветов: вы можете заставить PixelDroid следовать цвету вашего фона (Android 12 и выше) +* Исправление ошибок +* Обновление переводов diff --git a/fastlane/metadata/android/ru/changelogs/2.txt b/fastlane/metadata/android/ru/changelogs/2.txt new file mode 100644 index 00000000..0a6c7cfc --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/2.txt @@ -0,0 +1,7 @@ +* Обновления перевода: новые строки и обновления других строк. Спасибо переводчикам ❤️! Заходите на наш сайт, если хотите помочь с переводом на ваш язык. + +* #️⃣ Поддержка хэштегов. Теперь вы можете просматривать хэштеги, а не просто показывать сообщение с тостом ��. + +* Нажмите на вкладку, чтобы прокрутить страницу к началу. Больше нет необходимости в бешеной прокрутке, чтобы вернуться к началу. Просто нажмите на вкладку, в которой вы находитесь, и она прокрутится вверх :) + +* Попытка исправить некоторые ошибки, из-за которых приложение падало. Большое спасибо всем за сообщения о сбоях! ❤️ diff --git a/fastlane/metadata/android/ru/changelogs/20.txt b/fastlane/metadata/android/ru/changelogs/20.txt new file mode 100644 index 00000000..ef76d8fd --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/20.txt @@ -0,0 +1,4 @@ +- Удаление и редактирование существующих сообщений +- Коллекции собственных постов теперь можно просматривать и редактировать +- Создание постов теперь происходит в два этапа, с новой поддержкой: NSFW-чувствительность, переключение аккаунтов +- Много других изменений и улучшений :) diff --git a/fastlane/metadata/android/ru/changelogs/23.txt b/fastlane/metadata/android/ru/changelogs/23.txt new file mode 100644 index 00000000..9fb12156 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/23.txt @@ -0,0 +1,4 @@ +* Менее агрессивные предупреждения при отключении камеры или прав доступа к файлам +* Обновление переводов +* Исправление ошибок при загрузке видео +* Улучшена обработка ошибок diff --git a/fastlane/metadata/android/ru/changelogs/3.txt b/fastlane/metadata/android/ru/changelogs/3.txt new file mode 100644 index 00000000..da7dbd4f --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/3.txt @@ -0,0 +1,7 @@ +* Добавить язык (малаялам) + +* Исправление некоторых ошибок в ответах API + +* Обновление зависимостей + +* Обновить переводы diff --git a/fastlane/metadata/android/ru/changelogs/4.txt b/fastlane/metadata/android/ru/changelogs/4.txt new file mode 100644 index 00000000..113ef2b2 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/4.txt @@ -0,0 +1,9 @@ +- Поддержка уведомлений! Все еще немного рудиментарная, скоро будет доработка :) +- Исправление проблемы, из-за которой игнорировался поворот EXIF, в результате чего фотографии отображались повернутыми не в ту сторону +- Исправление #300 +- Исправление ошибки, из-за которой браузеры с веб-просмотром выдавали ошибку при входе, поскольку URL содержал пробелы, которые не были закодированы в URI +- Исправление проблемы, из-за которой кэш сбрасывался, а лента новостей оставалась пустой при каждом запуске приложения, что приводило к проблемам с производительностью +- Добавить чешский язык, обновили другие переводы +- Исправлены фотографии профиля +- Исправление неработающей камеры после переключения вкладок +- Обновление зависимостей diff --git a/fastlane/metadata/android/ru/changelogs/5.txt b/fastlane/metadata/android/ru/changelogs/5.txt new file mode 100644 index 00000000..0d7661cb --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/5.txt @@ -0,0 +1,5 @@ +* Обновления перевода +* Более легкий способ просмотра информации о лицензии +* Исправление проблем с разрешениями на вкладке камеры +* Исправление разбора ссылок +* Исправление просмотра открытий (может потребоваться обновление экземпляра) diff --git a/fastlane/metadata/android/ru/changelogs/7.txt b/fastlane/metadata/android/ru/changelogs/7.txt new file mode 100644 index 00000000..efcb3ff3 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/7.txt @@ -0,0 +1,2 @@ +* Обновление переводов +* Разрешите удаленные изображения при загрузке (например, Nextcloud) diff --git a/fastlane/metadata/android/ru/changelogs/8.txt b/fastlane/metadata/android/ru/changelogs/8.txt new file mode 100644 index 00000000..8b1503e1 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/8.txt @@ -0,0 +1,4 @@ +* Добавили поддержку воспроизведения и загрузки видео. +* Улучшение уведомлений +* Обновление переводов +* Обновление зависимостей diff --git a/fastlane/metadata/android/ru/changelogs/9.txt b/fastlane/metadata/android/ru/changelogs/9.txt new file mode 100644 index 00000000..094e5bc3 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/9.txt @@ -0,0 +1,3 @@ +* Устранение сбоя при отображении списка лицензий +* Значительно улучшена производительность при работе с профилем +* Обновление зависимостей diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt index 394d6250..00b9c64b 100644 --- a/fastlane/metadata/android/ru/full_description.txt +++ b/fastlane/metadata/android/ru/full_description.txt @@ -1,6 +1,6 @@ -PixelDroid это свободный клиент Pixelfed с открытым исходным кодом для Android. +PixelDroid - это свободный Android-клиент с открытым исходным кодом для Pixelfed, федеративной платформы для обмена изображениями. -Просматривайте летны и профили, загружайте новые посты, находите посты, взаимодействуйте с другими пользователями федиверса. +Просматривайте ленты и профили, загружайте новые посты, находите посты, взаимодействуйте с другими пользователями федиверса. • Поддержка мульти-аккаунта • Светлая и тёмная темы From 888c6328d95cf9b7626467f4cbd52e4ba8f6d4bd Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Fri, 3 Nov 2023 18:49:44 +0100 Subject: [PATCH 03/46] Super rudimentary support for stories --- app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 3 + .../java/org/pixeldroid/app/MainActivity.kt | 9 +- .../app/postCreation/PostCreationViewModel.kt | 6 +- .../org/pixeldroid/app/posts/HtmlUtils.kt | 4 +- .../pixeldroid/app/posts/StatusViewHolder.kt | 3 +- .../notifications/NotificationsFragment.kt | 3 +- .../pixeldroid/app/stories/StoriesActivity.kt | 110 + .../app/stories/StoriesViewModel.kt | 143 + .../pixeldroid/app/utils/api/PixelfedAPI.kt | 14 + .../app/utils/api/objects/StoryCarousel.kt | 35 + .../app/utils/di/ApplicationComponent.kt | 2 + app/src/main/res/drawable/pause.xml | 5 + app/src/main/res/drawable/play.xml | 5 + app/src/main/res/drawable/play_pause.xml | 8 + app/src/main/res/layout/activity_stories.xml | 79 + app/src/main/res/values/strings.xml | 1 + gradle/verification-metadata.xml | 6874 ----------------- 18 files changed, 426 insertions(+), 6888 deletions(-) create mode 100644 app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt create mode 100644 app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt create mode 100644 app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt create mode 100644 app/src/main/res/drawable/pause.xml create mode 100644 app/src/main/res/drawable/play.xml create mode 100644 app/src/main/res/drawable/play_pause.xml create mode 100644 app/src/main/res/layout/activity_stories.xml delete mode 100644 gradle/verification-metadata.xml diff --git a/app/build.gradle b/app/build.gradle index 6e133f07..270d3ffa 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -131,14 +131,14 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2' + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' /** * AndroidX dependencies: */ implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.core:core-splashscreen:1.0.0' - implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.core:core-splashscreen:1.0.1' + implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' @@ -156,8 +156,8 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8:2.6.1" implementation "androidx.annotation:annotation:1.6.0" implementation 'androidx.gridlayout:gridlayout:1.0.0' - implementation "androidx.activity:activity-ktx:1.7.0" - implementation 'androidx.fragment:fragment-ktx:1.5.6' + implementation "androidx.activity:activity-ktx:1.7.1" + implementation 'androidx.fragment:fragment-ktx:1.5.7' implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'androidx.media2:media2-widget:1.2.1' implementation 'androidx.media2:media2-player:1.2.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index beb4a316..92a39fe5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,9 @@ android:name=".posts.ReportActivity" android:screenOrientation="sensorPortrait" tools:ignore="LockedOrientationActivity" /> + + when (position){ 1 -> launchActivity(ProfileActivity()) 2 -> launchActivity(SettingsActivity()) 3 -> logOut() + 4 -> launchActivity(StoriesActivity()) } false } diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index 8cd7a3a8..b3ad94c0 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -195,8 +195,10 @@ class PostCreationViewModel( * and display it. */ val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - cursor.moveToFirst() - cursor.getLong(sizeIndex) + if(sizeIndex >= 0) { + cursor.moveToFirst() + cursor.getLong(sizeIndex) + } else null } ?: 0 } else { uri.toFile().length() diff --git a/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt b/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt index e56e64fe..01da0f5b 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt @@ -130,7 +130,7 @@ fun parseHTMLText( } -fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean, context: Context) { +fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Boolean) { val now = Date.from(Instant.now()).time try { @@ -140,7 +140,7 @@ fun setTextViewFromISO8601(date: Instant, textView: TextView, absoluteTime: Bool android.text.format.DateUtils.SECOND_IN_MILLIS, android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE).toString() - textView.text = if(absoluteTime) context.getString(R.string.posted_on).format(date) + textView.text = if(absoluteTime) textView.context.getString(R.string.posted_on).format(date) else formattedDate } catch (e: ParseException) { diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt index 39898014..376ab1c4 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt @@ -139,8 +139,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold setTextViewFromISO8601( status?.created_at!!, binding.postDate, - isActivity, - binding.root.context + isActivity ) binding.postDomain.text = status?.getStatusDomain(domain, binding.postDomain.context) diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt index 7dd5beea..b1469fe3 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/notifications/NotificationsFragment.kt @@ -221,8 +221,7 @@ class NotificationsFragment : CachedFeedFragment() { setTextViewFromISO8601( it, notificationTime, - false, - itemView.context + false ) } diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt new file mode 100644 index 00000000..8a6e90d6 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -0,0 +1,110 @@ +package org.pixeldroid.app.stories + +import android.graphics.drawable.Drawable +import android.os.Build +import android.os.Bundle +import androidx.activity.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.RequestOptions +import com.bumptech.glide.request.target.Target +import kotlinx.coroutines.launch +import org.pixeldroid.app.databinding.ActivityStoriesBinding +import org.pixeldroid.app.posts.setTextViewFromISO8601 +import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity + + +class StoriesActivity: BaseThemedWithoutBarActivity() { + + private lateinit var binding: ActivityStoriesBinding + + private lateinit var model: StoriesViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityStoriesBinding.inflate(layoutInflater) + setContentView(binding.root) + + val _model: StoriesViewModel by viewModels { + StoriesViewModelFactory(application) + } + model = _model + + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + model.uiState.collect { uiState -> + binding.pause.isSelected = uiState.paused + + uiState.age?.let { setTextViewFromISO8601(it, binding.storyAge, false) } + + uiState.profilePicture?.let { + Glide.with(binding.storyAuthorProfilePicture) + .load(it) + .apply(RequestOptions.circleCropTransform()) + .into(binding.storyAuthorProfilePicture) + } + + binding.storyAuthor.text = uiState.username + + uiState.imageList.getOrNull(uiState.currentImage)?.let { + Glide.with(binding.storyImage) + .load(it) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean, + ): Boolean = false + + override fun onResourceReady( + resource: Drawable?, + m: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean, + ): Boolean { + model.imageLoaded() + return false + } + }) + .into(binding.storyImage) + } + } + } + } + + model.count.observe(this) { state -> + // Render state in UI + model.uiState.value.durationList.getOrNull(model.uiState.value.currentImage)?.let { + val percent = 100 - ((state/it.toFloat())*100).toInt() + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + binding.progressBarStory.setProgress(percent, true) + } else { + binding.progressBarStory.progress = percent + } + } + } + + binding.pause.setOnClickListener { + //Set the button's appearance + it.isSelected = !it.isSelected + if (it.isSelected) { + //Handle selected state change + } else { + //Handle de-select state change + } + } + + binding.storyImage.setOnClickListener { + model.pause() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt new file mode 100644 index 00000000..18382e18 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt @@ -0,0 +1,143 @@ +package org.pixeldroid.app.stories + +import android.app.Application +import android.os.CountDownTimer +import android.util.Log +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.pixeldroid.app.utils.PixelDroidApplication +import org.pixeldroid.app.utils.api.objects.StoryCarousel +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import java.time.Instant +import javax.inject.Inject + +data class StoriesUiState( + val profilePicture: String? = null, + val username: String? = null, + val age: Instant? = null, + val currentImage: Int = 0, + val imageList: List = emptyList(), + val durationList: List = emptyList(), + val paused: Boolean = false, + val errorMessage: String? = null, +) + +class StoriesViewModel( + application: Application, +) : AndroidViewModel(application) { + + @Inject + lateinit var apiHolder: PixelfedAPIHolder + + private val _uiState: MutableStateFlow = MutableStateFlow(StoriesUiState()) + + val uiState: StateFlow = _uiState + + var carousel: StoryCarousel? = null + + val count = MutableLiveData() + + private var timer: CountDownTimer? = null + + init { + (application as PixelDroidApplication).getAppComponent().inject(this) + loadStories() + } + + private fun setTimer(timerLength: Long) { + count.value = timerLength + timer = object: CountDownTimer(timerLength * 1000, 500){ + + override fun onTick(millisUntilFinished: Long) { + count.value = millisUntilFinished / 1000 + Log.e("Timer second", "${count.value}") + } + + override fun onFinish() { + goToNext() + } + } + } + + private fun goToNext(){ + _uiState.update { currentUiState -> + currentUiState.copy( + currentImage = currentUiState.currentImage + 1, + //TODO don't just take the first here, choose from activity input somehow? + age = carousel?.nodes?.firstOrNull()?.nodes?.getOrNull(currentUiState.currentImage + 1)?.created_at + ) + } + //TODO when done with viewing all stories, close activity and move to profile (?) + timer?.cancel() + startTimerForCurrent() + } + + private fun loadStories() { + viewModelScope.launch { + try{ + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + carousel = api.carousel() + + //TODO don't just take the first here, choose from activity input somehow? + val chosenAccount = carousel?.nodes?.firstOrNull() + + _uiState.update { currentUiState -> + currentUiState.copy( + profilePicture = chosenAccount?.user?.avatar, + age = chosenAccount?.nodes?.getOrNull(0)?.created_at, + username = chosenAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option? + errorMessage = null, + currentImage = 0, + imageList = chosenAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(), + durationList = chosenAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList() + ) + } + startTimerForCurrent() + } catch (exception: Exception){ + _uiState.update { currentUiState -> + currentUiState.copy(errorMessage = "Something went wrong fetching the carousel") + } + } + } + } + + private fun startTimerForCurrent(){ + uiState.value.let { + it.durationList.getOrNull(it.currentImage)?.toLong()?.let { time -> + setTimer(time) + timer?.start() + } + } + } + + fun imageLoaded() {/* + _uiState.update { currentUiState -> + currentUiState.copy(currentImage = currentUiState.currentImage + 1) + }*/ + } + + fun pause() { + if(_uiState.value.paused){ + timer?.start() + } else { + timer?.cancel() + count.value?.let { setTimer(it) } + } + _uiState.update { currentUiState -> + currentUiState.copy(paused = !currentUiState.paused) + } + } +} + +class StoriesViewModelFactory(val application: Application) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return modelClass.getConstructor(Application::class.java).newInstance(application) + } +} diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index 4e8db5fa..f008e992 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -231,6 +231,20 @@ interface PixelfedAPI { @Query("post_id") post_id: String, ) + @GET("/api/v1.1/stories/carousel") + suspend fun carousel( + ): StoryCarousel + + @POST("/api/v1.1/stories/seen") + suspend fun carouselSeen( + @Query("id") id: String //TODO figure out if this is the id of post or of user? + ) + + @POST("/api/v1.1/stories/self-expire/{id}") + suspend fun deleteCarousel( + @Path("id") storyId: String + ) + //Used in our case to retrieve comments for a given status @GET("/api/v1/statuses/{id}/context") suspend fun statusComments( diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt new file mode 100644 index 00000000..12d5a799 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt @@ -0,0 +1,35 @@ +package org.pixeldroid.app.utils.api.objects + +import java.time.Instant + +data class StoryCarousel( + val self: CarouselUserContainer?, + val nodes: List? +) + +data class CarouselUser( + val id: String?, + val username: String?, + val username_acct: String?, + val avatar: String?, // URL to account avatar + val local: Boolean?, // Is this story from the local instance? + val is_author: Boolean?, // Is this me? (seems redundant with id) +) + +/** + * Container with a description of the [user] and a list of stories ([nodes]) + */ +data class CarouselUserContainer( + val user: CarouselUser?, + val nodes: List?, +) + +data class Story( + val id: String?, + val pid: String?, // id of author + val type: String?, //TODO make enum of this? examples: "photo", ??? + val src: String?, // URL to photo of story + val duration: Int?, //Time in seconds that the Story should be shown + val seen: Boolean?, //Indication of whether this story has been seen. Set to true using carouselSeen + val created_at: Instant?, //ISO 8601 Datetime +) \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt index b9a779bb..23aca907 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt @@ -9,6 +9,7 @@ import org.pixeldroid.app.utils.BaseFragment import dagger.Component import org.pixeldroid.app.postCreation.PostCreationViewModel import org.pixeldroid.app.profile.EditProfileViewModel +import org.pixeldroid.app.stories.StoriesViewModel import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker import javax.inject.Singleton @@ -22,6 +23,7 @@ interface ApplicationComponent { fun inject(notificationsWorker: NotificationsWorker) fun inject(postCreationViewModel: PostCreationViewModel) fun inject(editProfileViewModel: EditProfileViewModel) + fun inject(storiesViewModel: StoriesViewModel) val context: Context? val application: Application? diff --git a/app/src/main/res/drawable/pause.xml b/app/src/main/res/drawable/pause.xml new file mode 100644 index 00000000..f701d6f8 --- /dev/null +++ b/app/src/main/res/drawable/pause.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/play.xml b/app/src/main/res/drawable/play.xml new file mode 100644 index 00000000..0870be8f --- /dev/null +++ b/app/src/main/res/drawable/play.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/play_pause.xml b/app/src/main/res/drawable/play_pause.xml new file mode 100644 index 00000000..9c956cb2 --- /dev/null +++ b/app/src/main/res/drawable/play_pause.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_stories.xml b/app/src/main/res/layout/activity_stories.xml new file mode 100644 index 00000000..b61152df --- /dev/null +++ b/app/src/main/res/layout/activity_stories.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d56dece..e47bf398 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -330,4 +330,5 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Contains NSFW media Switch accounts NSFW/CW posts will not be blurred, and will be shown by default. + Story image diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml deleted file mode 100644 index fd01fe5f..00000000 --- a/gradle/verification-metadata.xml +++ /dev/null @@ -1,6874 +0,0 @@ - - - - true - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From f60889ea1457d33a1d9a0e5db7632375ce9326f9 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Tue, 9 May 2023 22:48:06 +0200 Subject: [PATCH 04/46] More work on stories --- .../pixeldroid/app/stories/StoriesActivity.kt | 43 +++++++++++-- .../app/stories/StoriesViewModel.kt | 43 +++++++++++++ .../pixeldroid/app/utils/api/PixelfedAPI.kt | 8 ++- app/src/main/res/layout/activity_stories.xml | 62 ++++++++++++++++++- app/src/main/res/values/strings.xml | 1 + 5 files changed, 149 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt index 8a6e90d6..3a85dfac 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -4,6 +4,8 @@ import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle import androidx.activity.viewModels +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle @@ -13,7 +15,9 @@ import com.bumptech.glide.load.engine.GlideException import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.Target +import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch +import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityStoriesBinding import org.pixeldroid.app.posts.setTextViewFromISO8601 import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity @@ -43,6 +47,23 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { uiState.age?.let { setTextViewFromISO8601(it, binding.storyAge, false) } + if (uiState.errorMessage != null) { + binding.storyErrorText.text = uiState.errorMessage + binding.storyErrorCard.isVisible = true + } else binding.storyErrorCard.isVisible = false + + if (uiState.snackBar != null) { + Snackbar.make( + binding.root, uiState.snackBar, + Snackbar.LENGTH_SHORT + ).setAnchorView(binding.storyReplyField).show() + model.shownSnackbar() + } + + if (uiState.username != null) { + binding.storyReplyField.hint = getString(R.string.replyToStory).format(uiState.username) + } else binding.storyReplyField.hint = null + uiState.profilePicture?.let { Glide.with(binding.storyAuthorProfilePicture) .load(it) @@ -79,6 +100,22 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { } } } + binding.storyReplyField.editText?.doAfterTextChanged { + it?.let { text -> + val string = text.toString() + if(string != model.uiState.value.reply) model.replyChanged(string) + } + } + + binding.storyReplyField.setEndIconOnClickListener { + binding.storyReplyField.editText?.text?.let { text -> + model.sendReply(text) + } + } + + binding.storyErrorCard.setOnClickListener{ + model.dismissError() + } model.count.observe(this) { state -> // Render state in UI @@ -96,11 +133,7 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { binding.pause.setOnClickListener { //Set the button's appearance it.isSelected = !it.isSelected - if (it.isSelected) { - //Handle selected state change - } else { - //Handle de-select state change - } + model.pause() } binding.storyImage.setOnClickListener { diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt index 18382e18..bf81567c 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt @@ -2,16 +2,19 @@ package org.pixeldroid.app.stories import android.app.Application import android.os.CountDownTimer +import android.text.Editable import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.pixeldroid.app.R import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.api.objects.StoryCarousel import org.pixeldroid.app.utils.di.PixelfedAPIHolder @@ -27,6 +30,8 @@ data class StoriesUiState( val durationList: List = emptyList(), val paused: Boolean = false, val errorMessage: String? = null, + val snackBar: String? = null, + val reply: String = "" ) class StoriesViewModel( @@ -134,6 +139,44 @@ class StoriesViewModel( currentUiState.copy(paused = !currentUiState.paused) } } + + fun sendReply(text: Editable) { + viewModelScope.launch { + try { + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + //TODO don't just take the first here, choose from activity input somehow? + val id = carousel?.nodes?.firstOrNull()?.nodes?.getOrNull(uiState.value.currentImage)?.id + id?.let { api.storyComment(it, text.toString()) } + + _uiState.update { currentUiState -> + currentUiState.copy(snackBar = "Sent reply") + } + } catch (exception: Exception){ + _uiState.update { currentUiState -> + currentUiState.copy(errorMessage = "Something went wrong sending reply") + } + } + + } + } + + fun replyChanged(text: String) { + _uiState.update { currentUiState -> + currentUiState.copy(reply = text) + } + } + + fun dismissError() { + _uiState.update { currentUiState -> + currentUiState.copy(errorMessage = null) + } + } + + fun shownSnackbar() { + _uiState.update { currentUiState -> + currentUiState.copy(snackBar = null) + } + } } class StoriesViewModelFactory(val application: Application) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index f008e992..6750dfef 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -236,10 +236,16 @@ interface PixelfedAPI { ): StoryCarousel @POST("/api/v1.1/stories/seen") - suspend fun carouselSeen( + suspend fun storySeen( @Query("id") id: String //TODO figure out if this is the id of post or of user? ) + @POST("/api/v1.1/stories/comment") + suspend fun storyComment( + @Query("sid") sid: String, + @Query("caption") caption: String + ) + @POST("/api/v1.1/stories/self-expire/{id}") suspend fun deleteCarousel( @Path("id") storyId: String diff --git a/app/src/main/res/layout/activity_stories.xml b/app/src/main/res/layout/activity_stories.xml index b61152df..8f733c90 100644 --- a/app/src/main/res/layout/activity_stories.xml +++ b/app/src/main/res/layout/activity_stories.xml @@ -6,16 +6,59 @@ android:background="@color/black" android:layout_height="match_parent"> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e47bf398..acabe04e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -331,4 +331,5 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Switch accounts NSFW/CW posts will not be blurred, and will be shown by default. Story image + Reply to %1$s From 73193abd95942863f89f4bbb3e03d20ffaffa370 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Sat, 13 May 2023 15:12:51 +0200 Subject: [PATCH 05/46] More progress on stories :) --- app/build.gradle | 4 +- .../org/pixeldroid/app/posts/HtmlUtils.kt | 3 +- .../searchDiscover/SearchDiscoverFragment.kt | 90 +- .../pixeldroid/app/stories/StoriesActivity.kt | 92 +- .../app/stories/StoriesViewModel.kt | 132 +- .../app/utils/api/objects/Account.kt | 12 +- .../app/utils/api/objects/StoryCarousel.kt | 9 +- app/src/main/res/layout/activity_stories.xml | 45 +- app/src/main/res/layout/fragment_search.xml | 65 +- app/src/main/res/layout/story_carousel.xml | 50 + app/src/main/res/values/strings.xml | 4 + gradle/verification-metadata.xml | 7949 +++++++++++++++++ 12 files changed, 8349 insertions(+), 106 deletions(-) create mode 100644 app/src/main/res/layout/story_carousel.xml create mode 100644 gradle/verification-metadata.xml diff --git a/app/build.gradle b/app/build.gradle index 270d3ffa..e875c441 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -138,7 +138,7 @@ dependencies { */ implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'androidx.core:core-splashscreen:1.0.1' - implementation 'androidx.core:core-ktx:1.10.0' + implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.preference:preference-ktx:1.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3' @@ -186,7 +186,7 @@ dependencies { implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' - implementation 'com.google.android.material:material:1.8.0' + implementation 'com.google.android.material:material:1.9.0' //Dagger (dependency injection) implementation 'com.google.dagger:dagger-android:2.45' diff --git a/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt b/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt index 01da0f5b..c60a8a0f 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/HtmlUtils.kt @@ -11,6 +11,7 @@ import android.view.View import android.widget.TextView import androidx.core.text.toSpanned import androidx.lifecycle.LifecycleCoroutineScope +import kotlinx.coroutines.launch import org.pixeldroid.app.R import org.pixeldroid.app.utils.api.PixelfedAPI import org.pixeldroid.app.utils.api.objects.Account.Companion.openAccountFromId @@ -106,7 +107,7 @@ fun parseHTMLText( override fun onClick(widget: View) { // Retrieve the account for the given profile - lifecycleScope.launchWhenCreated { + lifecycleScope.launch { val api: PixelfedAPI = apiHolder.api ?: apiHolder.setToCurrentUser() openAccountFromId(accountId, api, context) } diff --git a/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt b/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt index a1a73e51..3715c601 100644 --- a/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt @@ -1,5 +1,6 @@ package org.pixeldroid.app.searchDiscover +import android.annotation.SuppressLint import android.app.SearchManager import android.content.Context import android.content.Intent @@ -8,25 +9,38 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.google.android.material.carousel.CarouselLayoutManager +import kotlinx.coroutines.launch import org.pixeldroid.app.databinding.FragmentSearchBinding +import org.pixeldroid.app.databinding.StoryCarouselBinding import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TrendingType -import org.pixeldroid.app.utils.api.PixelfedAPI +import org.pixeldroid.app.stories.StoriesActivity +import org.pixeldroid.app.stories.StoriesActivity.Companion.STORY_CAROUSEL +import org.pixeldroid.app.stories.StoriesActivity.Companion.STORY_CAROUSEL_USER_ID import org.pixeldroid.app.utils.BaseFragment +import org.pixeldroid.app.utils.api.PixelfedAPI +import org.pixeldroid.app.utils.api.objects.CarouselUserContainer +import org.pixeldroid.app.utils.api.objects.StoryCarousel import org.pixeldroid.app.utils.bindingLifecycleAware + /** * This fragment lets you search and use Pixelfed's Discover feature */ class SearchDiscoverFragment : BaseFragment() { + private lateinit var api: PixelfedAPI var binding: FragmentSearchBinding by bindingLifecycleAware() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { binding = FragmentSearchBinding.inflate(inflater, container, false) @@ -37,6 +51,13 @@ class SearchDiscoverFragment : BaseFragment() { isSubmitButtonEnabled = true } + val adapter = StoriesListAdapter(::onClickStory) + binding.recyclerView2.adapter = adapter + + loadStories(adapter) + + binding.recyclerView2.layoutManager = CarouselLayoutManager() + return binding.root } @@ -56,4 +77,69 @@ class SearchDiscoverFragment : BaseFragment() { intent.putExtra(TRENDING_TAG, type) ContextCompat.startActivity(binding.root.context, intent, null) } + + private fun onClickStory(carousel: StoryCarousel, userId: String){ + val intent = Intent(requireContext(), StoriesActivity::class.java) + intent.putExtra(STORY_CAROUSEL, carousel) + intent.putExtra(STORY_CAROUSEL_USER_ID, userId) + startActivity(intent) + } + + private fun loadStories(adapter: StoriesListAdapter) { + lifecycleScope.launch { + try{ + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + val carousel = api.carousel() + adapter.initCarousel(carousel) + } catch (exception: Exception){ + //TODO + } + } + } + } + +class StoriesListAdapter(private val listener: (StoryCarousel, String) -> Unit): RecyclerView.Adapter() { + + private var storyCarousel: StoryCarousel? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val v = StoryCarouselBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolder(v) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + storyCarousel?.nodes?.get(position)?.let { holder.bindItem(it) } + holder.itemView.setOnClickListener { + storyCarousel?.let { carousel -> + storyCarousel?.nodes?.get(position)?.user?.id?.let { userId -> + listener( + carousel, + userId + ) + } + } + } + } + + override fun getItemCount(): Int { + return storyCarousel?.nodes?.size ?: 0 + } + + @SuppressLint("NotifyDataSetChanged") + fun initCarousel(carousel: StoryCarousel){ + storyCarousel = carousel + notifyDataSetChanged() + } + + + class ViewHolder(var itemBinding: StoryCarouselBinding) : + RecyclerView.ViewHolder(itemBinding.root) { + fun bindItem(user: CarouselUserContainer) { + Glide.with(itemBinding.root).load(user.nodes?.firstOrNull()?.src).into(itemBinding.carouselImageView) + Glide.with(itemBinding.root).load(user.user?.avatar).circleCrop().into(itemBinding.storyAuthorProfilePicture) + + itemBinding.username.text = user.user?.username ?: "" //TODO check which one to use here! + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt index 3a85dfac..893542ea 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -3,6 +3,9 @@ package org.pixeldroid.app.stories import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle +import android.view.MotionEvent +import android.view.View.OnClickListener +import android.view.View.OnTouchListener import androidx.activity.viewModels import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged @@ -21,10 +24,18 @@ import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityStoriesBinding import org.pixeldroid.app.posts.setTextViewFromISO8601 import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity +import org.pixeldroid.app.utils.api.objects.Account +import org.pixeldroid.app.utils.api.objects.StoryCarousel class StoriesActivity: BaseThemedWithoutBarActivity() { + companion object { + const val STORY_CAROUSEL = "LaunchStoryCarousel" + const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId" + } + + private lateinit var binding: ActivityStoriesBinding private lateinit var model: StoriesViewModel @@ -32,11 +43,14 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as StoryCarousel + val userId = intent.getStringExtra(STORY_CAROUSEL_USER_ID) + binding = ActivityStoriesBinding.inflate(layoutInflater) setContentView(binding.root) val _model: StoriesViewModel by viewModels { - StoriesViewModelFactory(application) + StoriesViewModelFactory(application, carousel, userId) } model = _model @@ -48,7 +62,7 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { uiState.age?.let { setTextViewFromISO8601(it, binding.storyAge, false) } if (uiState.errorMessage != null) { - binding.storyErrorText.text = uiState.errorMessage + binding.storyErrorText.setText(uiState.errorMessage) binding.storyErrorCard.isVisible = true } else binding.storyErrorCard.isVisible = false @@ -73,6 +87,9 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { binding.storyAuthor.text = uiState.username + binding.carouselProgress.text = getString(R.string.storyProgress) + .format(uiState.currentImage + 1, uiState.imageList.size) + uiState.imageList.getOrNull(uiState.currentImage)?.let { Glide.with(binding.storyImage) .load(it) @@ -91,7 +108,9 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { dataSource: DataSource?, isFirstResource: Boolean, ): Boolean { - model.imageLoaded() + Glide.with(binding.storyImage) + .load(uiState.imageList.getOrNull(uiState.currentImage + 1)) + .preload() return false } }) @@ -100,6 +119,19 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { } } } + + //Pause when clicked on text field + binding.storyReplyField.editText?.setOnFocusChangeListener { view, isFocused -> + if (view.isInTouchMode && isFocused) { + view.performClick() // picks up first tap + } + } + binding.storyReplyField.editText?.setOnClickListener { + if (!model.uiState.value.paused) { + model.pause() + } + } + binding.storyReplyField.editText?.doAfterTextChanged { it?.let { text -> val string = text.toString() @@ -134,10 +166,60 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { //Set the button's appearance it.isSelected = !it.isSelected model.pause() + } + + val authorOnClickListener = OnClickListener { + if (!model.uiState.value.paused) { + model.pause() + } + model.currentProfileId()?.let { + lifecycleScope.launch { + Account.openAccountFromId( + it, + apiHolder.api ?: apiHolder.setToCurrentUser(), + this@StoriesActivity + ) + } + } + } + binding.storyAuthorProfilePicture.setOnClickListener(authorOnClickListener) + binding.storyAuthor.setOnClickListener(authorOnClickListener) + + val onTouchListener = OnTouchListener { v, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> if (!model.uiState.value.paused) { + model.pause() + } + MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) { + v.performClick() + return@OnTouchListener false + } else model.pause() } - binding.storyImage.setOnClickListener { - model.pause() + true + } + + binding.viewMiddle.setOnTouchListener{ v, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> model.pause() + MotionEvent.ACTION_UP -> if(event.eventTime - event.downTime < 500) { + v.performClick() + return@setOnTouchListener false + } else model.pause() + } + + true + } + binding.viewLeft.setOnTouchListener(onTouchListener) + binding.viewRight.setOnTouchListener(onTouchListener) + + //TODO implement hold to pause + + binding.viewRight.setOnClickListener { + model.goToNext() + } + binding.viewLeft.setOnClickListener { + model.goToPrevious() } } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt index bf81567c..91a1b894 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt @@ -3,13 +3,12 @@ package org.pixeldroid.app.stories import android.app.Application import android.os.CountDownTimer import android.text.Editable -import android.util.Log +import androidx.annotation.StringRes import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -29,40 +28,45 @@ data class StoriesUiState( val imageList: List = emptyList(), val durationList: List = emptyList(), val paused: Boolean = false, - val errorMessage: String? = null, - val snackBar: String? = null, + @StringRes + val errorMessage: Int? = null, + @StringRes + val snackBar: Int? = null, val reply: String = "" ) class StoriesViewModel( application: Application, + val carousel: StoryCarousel, + userId: String? ) : AndroidViewModel(application) { @Inject lateinit var apiHolder: PixelfedAPIHolder - private val _uiState: MutableStateFlow = MutableStateFlow(StoriesUiState()) + private var currentAccount = carousel.nodes?.firstOrNull { it?.user?.id == userId } + + private val _uiState: MutableStateFlow = MutableStateFlow( + newUiStateFromCurrentAccount() + ) val uiState: StateFlow = _uiState - var carousel: StoryCarousel? = null - - val count = MutableLiveData() + val count = MutableLiveData() private var timer: CountDownTimer? = null init { (application as PixelDroidApplication).getAppComponent().inject(this) - loadStories() + startTimerForCurrent() } - private fun setTimer(timerLength: Long) { + private fun setTimer(timerLength: Float) { count.value = timerLength - timer = object: CountDownTimer(timerLength * 1000, 500){ + timer = object: CountDownTimer((timerLength * 1000).toLong(), 100){ override fun onTick(millisUntilFinished: Long) { - count.value = millisUntilFinished / 1000 - Log.e("Timer second", "${count.value}") + count.value = millisUntilFinished.toFloat() / 1000 } override fun onFinish() { @@ -71,63 +75,63 @@ class StoriesViewModel( } } - private fun goToNext(){ - _uiState.update { currentUiState -> - currentUiState.copy( - currentImage = currentUiState.currentImage + 1, - //TODO don't just take the first here, choose from activity input somehow? - age = carousel?.nodes?.firstOrNull()?.nodes?.getOrNull(currentUiState.currentImage + 1)?.created_at - ) + private fun newUiStateFromCurrentAccount(): StoriesUiState = StoriesUiState( + profilePicture = currentAccount?.user?.avatar, + age = currentAccount?.nodes?.getOrNull(0)?.created_at, + username = currentAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option? + errorMessage = null, + currentImage = 0, + imageList = currentAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(), + durationList = currentAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList() + ) + + private fun goTo(index: Int){ + if((0 until uiState.value.imageList.size).contains(index)) { + _uiState.update { currentUiState -> + currentUiState.copy( + currentImage = index, + age = currentAccount?.nodes?.getOrNull(index)?.created_at, + paused = false + ) + } + } else { + val currentUserId = currentAccount?.user?.id + val currentAccountIndex = carousel.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return + currentAccount = when (index) { + uiState.value.imageList.size -> { + // Go to next user + if(currentAccountIndex + 1 >= carousel.nodes.size) return + carousel.nodes.getOrNull(currentAccountIndex + 1) + + } + + -1 -> { + // Go to previous user + if(currentAccountIndex <= 0) return + carousel.nodes.getOrNull(currentAccountIndex - 1) + } + else -> return // Do nothing, given index does not make sense + } + _uiState.update { newUiStateFromCurrentAccount() } } - //TODO when done with viewing all stories, close activity and move to profile (?) + timer?.cancel() startTimerForCurrent() } - private fun loadStories() { - viewModelScope.launch { - try{ - val api = apiHolder.api ?: apiHolder.setToCurrentUser() - carousel = api.carousel() + fun goToNext() = goTo(uiState.value.currentImage + 1) - //TODO don't just take the first here, choose from activity input somehow? - val chosenAccount = carousel?.nodes?.firstOrNull() - - _uiState.update { currentUiState -> - currentUiState.copy( - profilePicture = chosenAccount?.user?.avatar, - age = chosenAccount?.nodes?.getOrNull(0)?.created_at, - username = chosenAccount?.user?.username, //TODO check if not username_acct, think about falling back on other option? - errorMessage = null, - currentImage = 0, - imageList = chosenAccount?.nodes?.mapNotNull { it?.src } ?: emptyList(), - durationList = chosenAccount?.nodes?.mapNotNull { it?.duration } ?: emptyList() - ) - } - startTimerForCurrent() - } catch (exception: Exception){ - _uiState.update { currentUiState -> - currentUiState.copy(errorMessage = "Something went wrong fetching the carousel") - } - } - } - } + fun goToPrevious() = goTo(uiState.value.currentImage - 1) private fun startTimerForCurrent(){ uiState.value.let { it.durationList.getOrNull(it.currentImage)?.toLong()?.let { time -> - setTimer(time) + setTimer(time.toFloat()) timer?.start() } } } - fun imageLoaded() {/* - _uiState.update { currentUiState -> - currentUiState.copy(currentImage = currentUiState.currentImage + 1) - }*/ - } - fun pause() { if(_uiState.value.paused){ timer?.start() @@ -144,16 +148,15 @@ class StoriesViewModel( viewModelScope.launch { try { val api = apiHolder.api ?: apiHolder.setToCurrentUser() - //TODO don't just take the first here, choose from activity input somehow? - val id = carousel?.nodes?.firstOrNull()?.nodes?.getOrNull(uiState.value.currentImage)?.id + val id = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)?.id id?.let { api.storyComment(it, text.toString()) } _uiState.update { currentUiState -> - currentUiState.copy(snackBar = "Sent reply") + currentUiState.copy(snackBar = R.string.sent_reply_story) } } catch (exception: Exception){ _uiState.update { currentUiState -> - currentUiState.copy(errorMessage = "Something went wrong sending reply") + currentUiState.copy(errorMessage = R.string.story_reply_error) } } @@ -177,10 +180,17 @@ class StoriesViewModel( currentUiState.copy(snackBar = null) } } + + fun currentProfileId(): String? = currentAccount?.user?.id + } -class StoriesViewModelFactory(val application: Application) : ViewModelProvider.Factory { +class StoriesViewModelFactory( + val application: Application, + val carousel: StoryCarousel, + val userId: String? +) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return modelClass.getConstructor(Application::class.java).newInstance(application) + return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java).newInstance(application, carousel, userId) } } diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Account.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Account.kt index d084799c..402d58dd 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Account.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Account.kt @@ -57,11 +57,13 @@ data class Account( suspend fun openAccountFromId(id: String, api : PixelfedAPI, context: Context) { val account = try { api.getAccount(id) - } catch (exception: IOException) { - Log.e("GET ACCOUNT ERROR", exception.toString()) - return - } catch (exception: HttpException) { - Log.e("ERROR CODE", exception.code().toString()) + } catch (exception: Exception) { + val toLog = if (exception is HttpException) { + exception.code().toString() + } else { + exception.toString() + } + Log.e("GET ACCOUNT ERROR", toLog) return } //Open the account page in a separate activity diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt index 12d5a799..73c22665 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt @@ -1,11 +1,12 @@ package org.pixeldroid.app.utils.api.objects +import java.io.Serializable import java.time.Instant data class StoryCarousel( val self: CarouselUserContainer?, val nodes: List? -) +): Serializable data class CarouselUser( val id: String?, @@ -14,7 +15,7 @@ data class CarouselUser( val avatar: String?, // URL to account avatar val local: Boolean?, // Is this story from the local instance? val is_author: Boolean?, // Is this me? (seems redundant with id) -) +): Serializable /** * Container with a description of the [user] and a list of stories ([nodes]) @@ -22,7 +23,7 @@ data class CarouselUser( data class CarouselUserContainer( val user: CarouselUser?, val nodes: List?, -) +): Serializable data class Story( val id: String?, @@ -32,4 +33,4 @@ data class Story( val duration: Int?, //Time in seconds that the Story should be shown val seen: Boolean?, //Indication of whether this story has been seen. Set to true using carouselSeen val created_at: Instant?, //ISO 8601 Datetime -) \ No newline at end of file +): Serializable \ No newline at end of file diff --git a/app/src/main/res/layout/activity_stories.xml b/app/src/main/res/layout/activity_stories.xml index 8f733c90..bce8f3c3 100644 --- a/app/src/main/res/layout/activity_stories.xml +++ b/app/src/main/res/layout/activity_stories.xml @@ -54,11 +54,13 @@ android:layout_width="match_parent" android:layout_height="0dp" android:contentDescription="@string/story_image" - tools:scaleType="centerCrop" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/progressBarStory" + app:layout_constraintVertical_bias="1.0" + tools:scaleType="centerCrop" tools:srcCompat="@tools:sample/backgrounds/scenic[10]" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 8fbf0e84..b1dddecd 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -1,9 +1,12 @@ - + android:layout_height="match_parent"> + + app:layout_constraintTop_toBottomOf="@+id/recyclerView2"> + android:textColor="?attr/colorOnSecondaryContainer" /> + android:textColor="?attr/colorOnSecondaryContainer" /> + app:layout_constraintTop_toBottomOf="@id/hashtagsCardView"> + android:textColor="?attr/colorOnSecondaryContainer" /> + app:layout_constraintTop_toBottomOf="@id/accountsCardView"> + android:textColor="?attr/colorOnSecondaryContainer" /> - \ No newline at end of file + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/story_carousel.xml b/app/src/main/res/layout/story_carousel.xml new file mode 100644 index 00000000..7eee812f --- /dev/null +++ b/app/src/main/res/layout/story_carousel.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index acabe04e..e496d166 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -332,4 +332,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" NSFW/CW posts will not be blurred, and will be shown by default. Story image Reply to %1$s + %1$s / %2$s + Something went wrong sending reply + Something went wrong fetching the carousel + Sent reply diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml new file mode 100644 index 00000000..1cd53ef5 --- /dev/null +++ b/gradle/verification-metadata.xml @@ -0,0 +1,7949 @@ + + + + true + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From dda06b1cd5dce736ee977f158f85c0dd11662d2a Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Fri, 2 Jun 2023 18:20:54 +0200 Subject: [PATCH 06/46] Stories progress --- .../java/org/pixeldroid/app/MainActivity.kt | 5 - .../cachedFeeds/postFeeds/PostFeedFragment.kt | 36 +- .../searchDiscover/SearchDiscoverFragment.kt | 83 ---- .../app/stories/StoryCarouselViewHolder.kt | 133 ++++++ app/src/main/res/layout/error_layout.xml | 12 +- app/src/main/res/layout/fragment_search.xml | 13 +- app/src/main/res/layout/story_carousel.xml | 63 +-- .../res/layout/story_carousel_add_story.xml | 46 ++ .../main/res/layout/story_carousel_item.xml | 50 ++ app/src/main/res/values/strings.xml | 1 + gradle/verification-metadata.xml | 429 ++++++++++++++++++ 11 files changed, 708 insertions(+), 163 deletions(-) create mode 100644 app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt create mode 100644 app/src/main/res/layout/story_carousel_add_story.xml create mode 100644 app/src/main/res/layout/story_carousel_item.xml diff --git a/app/src/main/java/org/pixeldroid/app/MainActivity.kt b/app/src/main/java/org/pixeldroid/app/MainActivity.kt index 9ec525b1..0317efdd 100644 --- a/app/src/main/java/org/pixeldroid/app/MainActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/MainActivity.kt @@ -231,17 +231,12 @@ class MainActivity : BaseThemedWithoutBarActivity() { nameRes = R.string.logout iconicsIcon = GoogleMaterial.Icon.gmd_close }, - primaryDrawerItem { - nameRes = R.string.story_image - iconicsIcon = GoogleMaterial.Icon.gmd_auto_stories - }, ) binding.drawer.onDrawerItemClickListener = { v, drawerItem, position -> when (position){ 1 -> launchActivity(ProfileActivity()) 2 -> launchActivity(SettingsActivity()) 3 -> logOut() - 4 -> launchActivity(StoriesActivity()) } false } diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt index 48c13fe4..28b77a80 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PostFeedFragment.kt @@ -12,13 +12,14 @@ import androidx.paging.RemoteMediator import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import org.pixeldroid.app.R -import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao import org.pixeldroid.app.posts.StatusViewHolder -import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel import org.pixeldroid.app.posts.feeds.cachedFeeds.CachedFeedFragment +import org.pixeldroid.app.posts.feeds.cachedFeeds.FeedViewModel import org.pixeldroid.app.posts.feeds.cachedFeeds.ViewModelFactory +import org.pixeldroid.app.stories.StoryCarouselViewHolder import org.pixeldroid.app.utils.api.objects.FeedContentDatabase import org.pixeldroid.app.utils.api.objects.Status +import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao import org.pixeldroid.app.utils.displayDimensionsInPx import kotlin.properties.Delegates @@ -40,7 +41,7 @@ class PostFeedFragment: CachedFeedFragment() { adapter = PostsAdapter(requireContext().displayDimensionsInPx()) - home = requireArguments().get("home") as Boolean + home = requireArguments().getBoolean("home") @Suppress("UNCHECKED_CAST") if (home){ @@ -55,7 +56,7 @@ class PostFeedFragment: CachedFeedFragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View? { val view = super.onCreateView(inflater, container, savedInstanceState) @@ -78,17 +79,34 @@ class PostFeedFragment: CachedFeedFragment() { ) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return StatusViewHolder.create(parent) + return if(viewType == R.layout.post_fragment){ + StatusViewHolder.create(parent) + } else { + StoryCarouselViewHolder.create(parent) + } } override fun getItemViewType(position: Int): Int { - return R.layout.post_fragment + return if(home && position == 0) R.layout.story_carousel + else R.layout.post_fragment } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val uiModel = getItem(position) as Status? - uiModel?.let { - (holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx) + if(home && position == 0){ + holder.itemView.visibility = View.GONE + holder.itemView.layoutParams = RecyclerView.LayoutParams(0, 0) + (holder as StoryCarouselViewHolder).bind(apiHolder, lifecycleScope, holder.itemView) + } else { + holder.itemView.visibility = View.VISIBLE + holder.itemView.layoutParams = + RecyclerView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + val uiModel = getItem(if(home) position - 1 else position) as Status? + uiModel?.let { + (holder as StatusViewHolder).bind(it, apiHolder, db, lifecycleScope, displayDimensionsInPx) + } } } } diff --git a/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt b/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt index 3715c601..b82a0598 100644 --- a/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/searchDiscover/SearchDiscoverFragment.kt @@ -1,6 +1,5 @@ package org.pixeldroid.app.searchDiscover -import android.annotation.SuppressLint import android.app.SearchManager import android.content.Context import android.content.Intent @@ -9,22 +8,11 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.RecyclerView -import com.bumptech.glide.Glide -import com.google.android.material.carousel.CarouselLayoutManager -import kotlinx.coroutines.launch import org.pixeldroid.app.databinding.FragmentSearchBinding -import org.pixeldroid.app.databinding.StoryCarouselBinding import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TRENDING_TAG import org.pixeldroid.app.searchDiscover.TrendingActivity.Companion.TrendingType -import org.pixeldroid.app.stories.StoriesActivity -import org.pixeldroid.app.stories.StoriesActivity.Companion.STORY_CAROUSEL -import org.pixeldroid.app.stories.StoriesActivity.Companion.STORY_CAROUSEL_USER_ID import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.api.PixelfedAPI -import org.pixeldroid.app.utils.api.objects.CarouselUserContainer -import org.pixeldroid.app.utils.api.objects.StoryCarousel import org.pixeldroid.app.utils.bindingLifecycleAware @@ -51,13 +39,6 @@ class SearchDiscoverFragment : BaseFragment() { isSubmitButtonEnabled = true } - val adapter = StoriesListAdapter(::onClickStory) - binding.recyclerView2.adapter = adapter - - loadStories(adapter) - - binding.recyclerView2.layoutManager = CarouselLayoutManager() - return binding.root } @@ -78,68 +59,4 @@ class SearchDiscoverFragment : BaseFragment() { ContextCompat.startActivity(binding.root.context, intent, null) } - private fun onClickStory(carousel: StoryCarousel, userId: String){ - val intent = Intent(requireContext(), StoriesActivity::class.java) - intent.putExtra(STORY_CAROUSEL, carousel) - intent.putExtra(STORY_CAROUSEL_USER_ID, userId) - startActivity(intent) - } - - private fun loadStories(adapter: StoriesListAdapter) { - lifecycleScope.launch { - try{ - val api = apiHolder.api ?: apiHolder.setToCurrentUser() - val carousel = api.carousel() - adapter.initCarousel(carousel) - } catch (exception: Exception){ - //TODO - } - } - } - } - -class StoriesListAdapter(private val listener: (StoryCarousel, String) -> Unit): RecyclerView.Adapter() { - - private var storyCarousel: StoryCarousel? = null - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val v = StoryCarouselBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ViewHolder(v) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - storyCarousel?.nodes?.get(position)?.let { holder.bindItem(it) } - holder.itemView.setOnClickListener { - storyCarousel?.let { carousel -> - storyCarousel?.nodes?.get(position)?.user?.id?.let { userId -> - listener( - carousel, - userId - ) - } - } - } - } - - override fun getItemCount(): Int { - return storyCarousel?.nodes?.size ?: 0 - } - - @SuppressLint("NotifyDataSetChanged") - fun initCarousel(carousel: StoryCarousel){ - storyCarousel = carousel - notifyDataSetChanged() - } - - - class ViewHolder(var itemBinding: StoryCarouselBinding) : - RecyclerView.ViewHolder(itemBinding.root) { - fun bindItem(user: CarouselUserContainer) { - Glide.with(itemBinding.root).load(user.nodes?.firstOrNull()?.src).into(itemBinding.carouselImageView) - Glide.with(itemBinding.root).load(user.user?.avatar).circleCrop().into(itemBinding.storyAuthorProfilePicture) - - itemBinding.username.text = user.user?.username ?: "" //TODO check which one to use here! - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt b/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt new file mode 100644 index 00000000..9323136c --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt @@ -0,0 +1,133 @@ +package org.pixeldroid.app.stories + +import android.annotation.SuppressLint +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import kotlinx.coroutines.launch +import org.pixeldroid.app.R +import org.pixeldroid.app.databinding.StoryCarouselAddStoryBinding +import org.pixeldroid.app.databinding.StoryCarouselBinding +import org.pixeldroid.app.databinding.StoryCarouselItemBinding +import org.pixeldroid.app.postCreation.carousel.dpToPx +import org.pixeldroid.app.utils.api.objects.CarouselUserContainer +import org.pixeldroid.app.utils.api.objects.StoryCarousel +import org.pixeldroid.app.utils.di.PixelfedAPIHolder + +class StoryCarouselViewHolder(val binding: StoryCarouselBinding) : RecyclerView.ViewHolder(binding.root) { + + fun bind( + pixelfedAPI: PixelfedAPIHolder, + lifecycleScope: LifecycleCoroutineScope, + itemView: View + ) { + val adapter = StoriesListAdapter() + binding.storyCarousel.adapter = adapter + + loadStories(adapter, lifecycleScope, pixelfedAPI, itemView) + } + + private fun loadStories( + adapter: StoriesListAdapter, + lifecycleScope: LifecycleCoroutineScope, + apiHolder: PixelfedAPIHolder, + itemView: View + ) { + lifecycleScope.launch { + try{ + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + val carousel = api.carousel() + + if (carousel.nodes?.isEmpty() != true) { + itemView.visibility = View.VISIBLE + itemView.layoutParams.height = 200.dpToPx(binding.root.context) + itemView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT + + adapter.initCarousel(carousel) + } + + } catch (exception: Exception){ + //TODO + } + } + } + + companion object { + fun create(parent: ViewGroup): StoryCarouselViewHolder { + val itemBinding = StoryCarouselBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return StoryCarouselViewHolder(itemBinding) + } + } +} + + +class StoriesListAdapter : RecyclerView.Adapter() { + + private var storyCarousel: StoryCarousel? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return if(viewType == R.layout.story_carousel_add_story){ + val v = StoryCarouselAddStoryBinding.inflate(LayoutInflater.from(parent.context), parent, false) + AddViewHolder(v) + } + else { + val v = StoryCarouselItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ViewHolder(v) + } + } + + override fun getItemViewType(position: Int): Int { + return if(position == 0) R.layout.story_carousel_add_story + else R.layout.story_carousel_item + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if(position > 0) { + val carouselPosition = position - 1 + storyCarousel?.nodes?.get(carouselPosition)?.let { (holder as ViewHolder).bindItem(it) } + holder.itemView.setOnClickListener { + storyCarousel?.let { carousel -> + storyCarousel?.nodes?.get(carouselPosition)?.user?.id?.let { userId -> + val intent = Intent(holder.itemView.context, StoriesActivity::class.java) + intent.putExtra(StoriesActivity.STORY_CAROUSEL, carousel) + intent.putExtra(StoriesActivity.STORY_CAROUSEL_USER_ID, userId) + holder.itemView.context.startActivity(intent) + } + } + } + } else { + holder.itemView.setOnClickListener { + //TODO support for adding a story + } + } + } + + override fun getItemCount(): Int { + // If the storyCarousel is not set, the carousel is not shown, so itemCount of 0 + return (storyCarousel?.nodes?.size?.plus(1)) ?: 0 + } + + @SuppressLint("NotifyDataSetChanged") + fun initCarousel(carousel: StoryCarousel){ + storyCarousel = carousel + notifyDataSetChanged() + } + + class AddViewHolder(itemBinding: StoryCarouselAddStoryBinding) : RecyclerView.ViewHolder(itemBinding.root) + + class ViewHolder(private val itemBinding: StoryCarouselItemBinding) : + RecyclerView.ViewHolder(itemBinding.root) { + fun bindItem(user: CarouselUserContainer) { + Glide.with(itemBinding.root).load(user.nodes?.firstOrNull()?.src).into(itemBinding.carouselImageView) + Glide.with(itemBinding.root).load(user.user?.avatar).circleCrop().into(itemBinding.storyAuthorProfilePicture) + + itemBinding.username.text = user.user?.username ?: "" //TODO check which one to use here! + } + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/error_layout.xml b/app/src/main/res/layout/error_layout.xml index 8925f2f2..8c8d19e4 100644 --- a/app/src/main/res/layout/error_layout.xml +++ b/app/src/main/res/layout/error_layout.xml @@ -1,17 +1,17 @@ - + tools:visibility="visible"> + app:layout_constraintTop_toBottomOf="@+id/search"> - - \ No newline at end of file diff --git a/app/src/main/res/layout/story_carousel.xml b/app/src/main/res/layout/story_carousel.xml index 7eee812f..6c3bb87b 100644 --- a/app/src/main/res/layout/story_carousel.xml +++ b/app/src/main/res/layout/story_carousel.xml @@ -1,50 +1,17 @@ - - - - - - - - - - - - - + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/story_carousel" + android:layout_width="match_parent" + android:paddingVertical="8dp" + android:layout_height="216dp" + tools:listitem="@layout/story_carousel_item" + android:clipChildren="false" + android:clipToPadding="false" + android:orientation="horizontal" + app:layoutManager="com.google.android.material.carousel.CarouselLayoutManager" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/search" /> diff --git a/app/src/main/res/layout/story_carousel_add_story.xml b/app/src/main/res/layout/story_carousel_add_story.xml new file mode 100644 index 00000000..9b0e82c9 --- /dev/null +++ b/app/src/main/res/layout/story_carousel_add_story.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/story_carousel_item.xml b/app/src/main/res/layout/story_carousel_item.xml new file mode 100644 index 00000000..5e77571c --- /dev/null +++ b/app/src/main/res/layout/story_carousel_item.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e496d166..37e8f5aa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -336,4 +336,5 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Something went wrong sending reply Something went wrong fetching the carousel Sent reply + Add Story diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 1cd53ef5..c014f310 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -483,6 +483,9 @@ + + + @@ -513,6 +516,9 @@ + + + @@ -661,6 +667,17 @@ + + + + + + + + + + + @@ -709,6 +726,14 @@ + + + + + + + + @@ -757,6 +782,14 @@ + + + + + + + + @@ -805,6 +838,14 @@ + + + + + + + + @@ -871,6 +912,17 @@ + + + + + + + + + + + @@ -937,6 +989,17 @@ + + + + + + + + + + + @@ -1003,6 +1066,17 @@ + + + + + + + + + + + @@ -2743,6 +2817,14 @@ + + + + + + + + @@ -2791,6 +2873,14 @@ + + + + + + + + @@ -2839,6 +2929,14 @@ + + + + + + + + @@ -2887,6 +2985,14 @@ + + + + + + + + @@ -2935,6 +3041,14 @@ + + + + + + + + @@ -3015,6 +3129,14 @@ + + + + + + + + @@ -3063,6 +3185,14 @@ + + + + + + + + @@ -3111,6 +3241,14 @@ + + + + + + + + @@ -3159,6 +3297,14 @@ + + + + + + + + @@ -3207,6 +3353,14 @@ + + + + + + + + @@ -3255,6 +3409,14 @@ + + + + + + + + @@ -3303,6 +3465,14 @@ + + + + + + + + @@ -3351,6 +3521,14 @@ + + + + + + + + @@ -3399,6 +3577,14 @@ + + + + + + + + @@ -3447,6 +3633,14 @@ + + + + + + + + @@ -3495,6 +3689,14 @@ + + + + + + + + @@ -3543,6 +3745,14 @@ + + + + + + + + @@ -3591,6 +3801,14 @@ + + + + + + + + @@ -3639,6 +3857,14 @@ + + + + + + + + @@ -3687,6 +3913,14 @@ + + + + + + + + @@ -3735,6 +3969,14 @@ + + + + + + + + @@ -3783,6 +4025,14 @@ + + + + + + + + @@ -3839,6 +4089,14 @@ + + + + + + + + @@ -3887,6 +4145,14 @@ + + + + + + + + @@ -3935,6 +4201,14 @@ + + + + + + + + @@ -3983,6 +4257,14 @@ + + + + + + + + @@ -4055,6 +4337,14 @@ + + + + + + + + @@ -4103,6 +4393,14 @@ + + + + + + + + @@ -4151,6 +4449,14 @@ + + + + + + + + @@ -4199,6 +4505,14 @@ + + + + + + + + @@ -4247,6 +4561,14 @@ + + + + + + + + @@ -4295,6 +4617,14 @@ + + + + + + + + @@ -4343,6 +4673,14 @@ + + + + + + + + @@ -4391,6 +4729,14 @@ + + + + + + + + @@ -4439,6 +4785,14 @@ + + + + + + + + @@ -4487,6 +4841,14 @@ + + + + + + + + @@ -4535,6 +4897,14 @@ + + + + + + + + @@ -4583,6 +4953,14 @@ + + + + + + + + @@ -4631,6 +5009,14 @@ + + + + + + + + @@ -4679,6 +5065,14 @@ + + + + + + + + @@ -4727,6 +5121,14 @@ + + + + + + + + @@ -4775,6 +5177,14 @@ + + + + + + + + @@ -4823,6 +5233,14 @@ + + + + + + + + @@ -4871,6 +5289,14 @@ + + + + + + + + @@ -5126,6 +5552,9 @@ + + + From ae54b83ec7f330596b7eb0d7077838508d6053a8 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Mon, 5 Jun 2023 21:38:20 +0200 Subject: [PATCH 07/46] Small improvement to error showing --- .../pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt | 6 ++++++ .../feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt | 2 +- .../feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt | 2 +- .../main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt | 6 +++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt index b7b0d436..f6158085 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/CommonFeedFragmentUtils.kt @@ -13,6 +13,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.gson.Gson +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -80,6 +81,11 @@ internal fun initAdapter( ?: loadState.append as? LoadState.Error ?: loadState.prepend as? LoadState.Error ?: loadState.refresh as? LoadState.Error + + if(errorState?.error is CancellationException){ + return@addLoadStateListener + } + errorState?.let { val error: String = (it.error as? HttpException)?.response()?.errorBody()?.string()?.ifEmpty { null }?.let { s -> try { diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt index 41e2f29f..c0b974ba 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt @@ -47,7 +47,7 @@ class HomeFeedRemoteMediator @Inject constructor( HomeStatusDatabaseEntity(user.user_id, user.instance_uri, it) } - val endOfPaginationReached = apiResponse.isEmpty() + val endOfPaginationReached = apiResponse.isEmpty() || maxId == apiResponse.sortedBy { it.created_at }.last().id db.withTransaction { // Clear table in the database diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt index fd5f19e9..9651ff0e 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt @@ -62,7 +62,7 @@ class PublicFeedRemoteMediator @Inject constructor( val dbObjects = apiResponse.map{ PublicFeedStatusDatabaseEntity(user.user_id, user.instance_uri, it) } - val endOfPaginationReached = apiResponse.isEmpty() + val endOfPaginationReached = apiResponse.isEmpty() || maxId == apiResponse.sortedBy { it.created_at }.last().id db.withTransaction { // Clear table in the database diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index 6750dfef..7598f0d3 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -23,6 +23,7 @@ import retrofit2.converter.gson.GsonConverterFactory import retrofit2.http.* import retrofit2.http.Field import java.time.Instant +import java.util.concurrent.TimeUnit /* @@ -51,7 +52,9 @@ interface PixelfedAPI { .client( OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor) // Only do secure-ish TLS connections (no HTTP or very old SSL/TLS) - .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)).build() + .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) + .readTimeout(20, TimeUnit.SECONDS) + .build() ) .build().create(PixelfedAPI::class.java) } @@ -74,6 +77,7 @@ interface PixelfedAPI { OkHttpClient().newBuilder().addNetworkInterceptor(headerInterceptor) // Only do secure-ish TLS connections (no HTTP or very old SSL/TLS) .connectionSpecs(listOf(ConnectionSpec.MODERN_TLS)) + .readTimeout(20, TimeUnit.SECONDS) .authenticator(TokenAuthenticator(user, db, pixelfedAPIHolder)) .addInterceptor { it.request().newBuilder().run { From db3da57b7be04c6348bd87a5cda851325281f84e Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Mon, 19 Jun 2023 23:06:09 +0200 Subject: [PATCH 08/46] implement story creation --- .../app/postCreation/PostCreationActivity.kt | 1 + .../app/postCreation/PostCreationFragment.kt | 8 +- .../app/postCreation/PostCreationViewModel.kt | 38 +- .../postCreation/PostSubmissionFragment.kt | 12 +- .../app/postCreation/camera/CameraActivity.kt | 11 +- .../app/postCreation/camera/CameraFragment.kt | 11 +- .../postCreation/carousel/ImageCarousel.kt | 23 +- .../pixeldroid/app/stories/StoriesActivity.kt | 6 +- .../app/stories/StoriesViewModel.kt | 23 +- .../app/stories/StoryCarouselViewHolder.kt | 6 +- .../pixeldroid/app/utils/api/PixelfedAPI.kt | 23 +- .../app/utils/api/objects/Attachment.kt | 6 + app/src/main/res/layout/activity_stories.xml | 52 +- .../res/layout/fragment_post_creation.xml | 2 +- app/src/main/res/values/strings.xml | 2 + app/src/main/res/values/styles.xml | 1 + gradle/verification-metadata.xml | 8378 ----------------- 17 files changed, 160 insertions(+), 8443 deletions(-) delete mode 100644 gradle/verification-metadata.xml diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt index 634546d2..a9c09aa6 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt @@ -1,6 +1,7 @@ package org.pixeldroid.app.postCreation import android.os.* +import androidx.core.view.WindowCompat import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import org.pixeldroid.app.R diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt index 9c7bc3f6..a460f46c 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.launch import org.pixeldroid.app.R import org.pixeldroid.app.databinding.FragmentPostCreationBinding import org.pixeldroid.app.postCreation.camera.CameraActivity +import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.postCreation.carousel.CarouselItem import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity @@ -83,11 +84,16 @@ class PostCreationFragment : BaseFragment() { requireActivity().intent.clipData!!, instance, requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION), - requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false) + requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false), + requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false), ) } model = _model + if(model.storyCreation){ + binding.carousel.showCaption = false + } + model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData -> // update UI binding.carousel.addData( diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index b3ad94c0..38b6474c 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -22,6 +22,7 @@ import androidx.preference.PreferenceManager import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException import com.jarsilio.android.scrambler.stripMetadata import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.flow.MutableStateFlow @@ -107,7 +108,8 @@ class PostCreationViewModel( clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null, existingDescription: String? = null, - existingNSFW: Boolean = false + existingNSFW: Boolean = false, + val storyCreation: Boolean = false, ) : AndroidViewModel(application) { private val photoData: MutableLiveData> by lazy { MutableLiveData>().also { @@ -444,7 +446,11 @@ class PostCreationViewModel( apiHolder.setToCurrentUser(it) } ?: apiHolder.api ?: apiHolder.setToCurrentUser() - val inter = api.mediaUpload(description, requestBody.parts[0]) + val inter: Observable = + //TODO specify story duration + //TODO validate that image is correct (?) aspect ratio + if (storyCreation) api.storyUpload(requestBody.parts[0]) + else api.mediaUpload(description, requestBody.parts[0]) apiHolder.api = null postSub = inter @@ -453,7 +459,11 @@ class PostCreationViewModel( .subscribe( { attachment: Attachment -> data.progress = 0 - data.uploadId = attachment.id!! + data.uploadId = if(storyCreation){ + attachment.media_id!! + } else { + attachment.id!! + } }, { e: Throwable -> _uiState.update { currentUiState -> @@ -509,11 +519,19 @@ class PostCreationViewModel( apiHolder.setToCurrentUser(it) } ?: apiHolder.api ?: apiHolder.setToCurrentUser() - api.postStatus( - statusText = description, - media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(), - sensitive = nsfw - ) + if(storyCreation){ + api.storyPublish( + media_id = getPhotoData().value!!.firstNotNullOf { it.uploadId }, + can_react = "1", can_reply = "1", + duration = 10 + ) + } else{ + api.postStatus( + statusText = description, + media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(), + sensitive = nsfw + ) + } Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_success), Toast.LENGTH_SHORT).show() val intent = Intent(getApplication(), MainActivity::class.java) @@ -555,8 +573,8 @@ class PostCreationViewModel( } } -class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory { +class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean, val storyCreation: Boolean) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW) + return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW, storyCreation) } } diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt index 4ce76ab0..2c1b68c8 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt @@ -20,6 +20,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.launch import org.pixeldroid.app.R import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding +import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity @@ -68,7 +69,8 @@ class PostSubmissionFragment : BaseFragment() { requireActivity().intent.clipData!!, instance, requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION), - requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false) + requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false), + requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false) ) } model = _model @@ -77,6 +79,14 @@ class PostSubmissionFragment : BaseFragment() { binding.nsfwSwitch.isChecked = model.uiState.value.nsfw binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText) + if(model.storyCreation){ + binding.nsfwSwitch.visibility = View.GONE + binding.postTextInputLayout.visibility = View.GONE + binding.privateTitle.visibility = View.GONE + binding.postPreview.visibility = View.GONE + //TODO show story specific stuff here + } + lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { model.uiState.collect { uiState -> diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt index 8ea1eff6..cd768f60 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraActivity.kt @@ -5,6 +5,8 @@ import android.os.Bundle import android.view.MenuItem import org.pixeldroid.app.MainActivity import org.pixeldroid.app.R +import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY +import org.pixeldroid.app.postCreation.camera.CameraFragment.Companion.CAMERA_ACTIVITY_STORY import org.pixeldroid.app.utils.BaseThemedWithBarActivity @@ -13,12 +15,17 @@ class CameraActivity : BaseThemedWithBarActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_camera) supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setTitle(R.string.add_photo) val cameraFragment = CameraFragment() + val story: Boolean = intent.getBooleanExtra(CAMERA_ACTIVITY_STORY, false) + + if(story) supportActionBar?.setTitle(R.string.add_story) + else supportActionBar?.setTitle(R.string.add_photo) + val arguments = Bundle() - arguments.putBoolean("CameraActivity", true) + arguments.putBoolean(CAMERA_ACTIVITY, true) + arguments.putBoolean(CAMERA_ACTIVITY_STORY, story) cameraFragment.arguments = arguments supportFragmentManager.beginTransaction() diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt index e492def2..00abfb97 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt @@ -70,6 +70,7 @@ class CameraFragment : BaseFragment() { private var camera: Camera? = null private var inActivity by Delegates.notNull() + private var addToStory by Delegates.notNull() private var filePermissionDialogLaunched: Boolean = false @@ -89,7 +90,8 @@ class CameraFragment : BaseFragment() { savedInstanceState: Bundle? ): View { super.onCreateView(inflater, container, savedInstanceState) - inActivity = arguments?.getBoolean("CameraActivity") ?: false + inActivity = arguments?.getBoolean(CAMERA_ACTIVITY) ?: false + addToStory = arguments?.getBoolean(CAMERA_ACTIVITY_STORY) ?: false binding = FragmentCameraBinding.inflate(layoutInflater) @@ -464,15 +466,20 @@ class CameraFragment : BaseFragment() { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } - if(inActivity){ + if(inActivity && !addToStory){ requireActivity().setResult(Activity.RESULT_OK, intent) requireActivity().finish() } else { + if(addToStory){ + intent.putExtra(CAMERA_ACTIVITY_STORY, addToStory) + } startActivity(intent) } } companion object { + const val CAMERA_ACTIVITY = "CameraActivity" + const val CAMERA_ACTIVITY_STORY = "CameraActivityStory" private const val TAG = "CameraFragment" private const val RATIO_4_3_VALUE = 4.0 / 3.0 diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt index 6c977864..e2e234ce 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt @@ -40,7 +40,6 @@ class ImageCarousel( ) private lateinit var recyclerView: RecyclerView - private lateinit var tvCaption: TextView private var snapHelper: SnapHelper = PagerSnapHelper() var indicator: CircleIndicator2? = null @@ -103,11 +102,12 @@ class ImageCarousel( * **************************************************************** */ + //FIXME this is modified a bunch of times all over the place, so it can't be set to false and stay there var showCaption = false set(value) { field = value - tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE + binding.tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE } @Dimension(unit = Dimension.PX) @@ -115,7 +115,7 @@ class ImageCarousel( set(value) { field = value - tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat()) + binding.tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat()) } var showIndicator = false @@ -245,14 +245,14 @@ class ImageCarousel( showNavigationButtons = showNavigationButtons binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE - tvCaption.visibility = if(editingMediaDescription) INVISIBLE else VISIBLE + showCaption = !editingMediaDescription } else { recyclerView.layoutManager = GridLayoutManager(context, 3) binding.btnNext.visibility = GONE binding.btnPrevious.visibility = GONE binding.editMediaDescriptionLayout.visibility = INVISIBLE - tvCaption.visibility = INVISIBLE + showCaption = false } showIndicator = value @@ -279,7 +279,7 @@ class ImageCarousel( updateDescriptionCallback?.invoke(currentPosition, description) } binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE - tvCaption.visibility = if(value) INVISIBLE else VISIBLE + showCaption = !value } @@ -289,10 +289,10 @@ class ImageCarousel( set(value) { if(!value.isNullOrEmpty()) { field = value - tvCaption.text = value + binding.tvCaption.text = value } else { field = null - tvCaption.text = context.getText(R.string.no_media_description) + binding.tvCaption.text = context.getText(R.string.no_media_description) } } @@ -317,12 +317,11 @@ class ImageCarousel( binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this, true) recyclerView = binding.recyclerView - tvCaption = binding.tvCaption recyclerView.setHasFixedSize(true) // For marquee effect - tvCaption.isSelected = true + binding.tvCaption.isSelected = true } @@ -441,7 +440,7 @@ class ImageCarousel( caption.apply { if(layoutCarousel){ binding.editMediaDescriptionLayout.visibility = INVISIBLE - tvCaption.visibility = VISIBLE + showCaption = true } currentDescription = this } @@ -472,7 +471,7 @@ class ImageCarousel( } }) - tvCaption.setOnClickListener { + binding.tvCaption.setOnClickListener { editingMediaDescription = true } diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt index 893542ea..ca7253c8 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -7,6 +7,7 @@ import android.view.MotionEvent import android.view.View.OnClickListener import android.view.View.OnTouchListener import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatDelegate import androidx.core.view.isVisible import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.Lifecycle @@ -41,6 +42,9 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { private lateinit var model: StoriesViewModel override fun onCreate(savedInstanceState: Bundle?) { + //force night mode always + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES + super.onCreate(savedInstanceState) val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as StoryCarousel @@ -213,8 +217,6 @@ class StoriesActivity: BaseThemedWithoutBarActivity() { binding.viewLeft.setOnTouchListener(onTouchListener) binding.viewRight.setOnTouchListener(onTouchListener) - //TODO implement hold to pause - binding.viewRight.setOnClickListener { model.goToNext() } diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt index 91a1b894..602019cb 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt @@ -4,11 +4,14 @@ import android.app.Application import android.os.CountDownTimer import android.text.Editable import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -119,7 +122,20 @@ class StoriesViewModel( startTimerForCurrent() } - fun goToNext() = goTo(uiState.value.currentImage + 1) + fun goToNext() { + viewModelScope.launch { + try { + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + currentStoryId()?.let { api.storySeen(it) } + } catch (exception: Exception){ + _uiState.update { currentUiState -> + currentUiState.copy(errorMessage = R.string.story_could_not_see) + } + } + + } + goTo(uiState.value.currentImage + 1) + } fun goToPrevious() = goTo(uiState.value.currentImage - 1) @@ -148,8 +164,7 @@ class StoriesViewModel( viewModelScope.launch { try { val api = apiHolder.api ?: apiHolder.setToCurrentUser() - val id = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)?.id - id?.let { api.storyComment(it, text.toString()) } + currentStoryId()?.let { api.storyComment(it, text.toString()) } _uiState.update { currentUiState -> currentUiState.copy(snackBar = R.string.sent_reply_story) @@ -163,6 +178,8 @@ class StoriesViewModel( } } + private fun currentStoryId(): String? = currentAccount?.nodes?.getOrNull(uiState.value.currentImage)?.id + fun replyChanged(text: String) { _uiState.update { currentUiState -> currentUiState.copy(reply = text) diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt b/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt index 9323136c..89c6626d 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt @@ -13,6 +13,8 @@ import org.pixeldroid.app.R import org.pixeldroid.app.databinding.StoryCarouselAddStoryBinding import org.pixeldroid.app.databinding.StoryCarouselBinding import org.pixeldroid.app.databinding.StoryCarouselItemBinding +import org.pixeldroid.app.postCreation.camera.CameraActivity +import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.postCreation.carousel.dpToPx import org.pixeldroid.app.utils.api.objects.CarouselUserContainer import org.pixeldroid.app.utils.api.objects.StoryCarousel @@ -103,7 +105,9 @@ class StoriesListAdapter : RecyclerView.Adapter() { } } else { holder.itemView.setOnClickListener { - //TODO support for adding a story + val intent = Intent(holder.itemView.context, CameraActivity::class.java) + intent.putExtra(CameraFragment.CAMERA_ACTIVITY_STORY, true) + holder.itemView.context.startActivity(intent) } } } diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index 7598f0d3..49bcacf2 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -236,12 +236,11 @@ interface PixelfedAPI { ) @GET("/api/v1.1/stories/carousel") - suspend fun carousel( - ): StoryCarousel + suspend fun carousel(): StoryCarousel @POST("/api/v1.1/stories/seen") suspend fun storySeen( - @Query("id") id: String //TODO figure out if this is the id of post or of user? + @Query("id") id: String ) @POST("/api/v1.1/stories/comment") @@ -250,6 +249,24 @@ interface PixelfedAPI { @Query("caption") caption: String ) + @Multipart + @POST("/api/v1.1/stories/add") + fun storyUpload( + @Part file: MultipartBody.Part, + @Part duration: MultipartBody.Part? = null, + ): Observable + + @POST("/api/v1.1/stories/publish") + suspend fun storyPublish( + @Query("media_id") media_id: String, + //From 0 to 30 + //TODO figure out what this duration means + @Query("duration") duration: Int = 10, + //FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works. Same issue as sensitive boolean in postStatus + @Query("can_reply") can_reply: String, + @Query("can_react") can_react: String, + ) + @POST("/api/v1.1/stories/self-expire/{id}") suspend fun deleteCarousel( @Path("id") storyId: String diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Attachment.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Attachment.kt index ad924fff..16c5aea9 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/Attachment.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/Attachment.kt @@ -18,6 +18,12 @@ data class Attachment( //Deprecated attributes val text_url: String? = null, //URL + + //Pixelfed's Story upload response... TODO make the server return a regular Attachment? + val msg: String?, + val media_id: String?, + val media_url: String?, + val media_type: String?, ) : Serializable { enum class AttachmentType: Serializable { unknown, image, gifv, video, audio diff --git a/app/src/main/res/layout/activity_stories.xml b/app/src/main/res/layout/activity_stories.xml index bce8f3c3..957891e1 100644 --- a/app/src/main/res/layout/activity_stories.xml +++ b/app/src/main/res/layout/activity_stories.xml @@ -3,8 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:background="@color/black" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:background="@color/black"> + android:layout_marginEnd="8dp" + android:minHeight="48dp"> + app:layout_constraintTop_toTopOf="parent" + tools:progress="56" /> + tools:srcCompat="@tools:sample/avatars" /> @@ -115,20 +114,20 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="12dp" - app:layout_constraintBottom_toBottomOf="@+id/storyAuthorProfilePicture" + app:layout_constraintBottom_toBottomOf="@+id/storyAuthor" app:layout_constraintStart_toEndOf="@+id/storyAuthor" app:layout_constraintTop_toTopOf="@+id/storyAuthorProfilePicture" tools:text="48m" /> + app:endIconMode="custom" + app:layout_constraintBottom_toBottomOf="parent" + tools:hint="Reply to PixelDroid"> @@ -160,9 +159,9 @@ android:id="@+id/viewMiddle" android:layout_width="80dp" android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@id/storyReplyField" app:layout_constraintEnd_toStartOf="@id/viewRight" app:layout_constraintStart_toEndOf="@id/viewLeft" - app:layout_constraintBottom_toTopOf="@id/storyReplyField" app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" /> @@ -170,11 +169,10 @@ android:id="@+id/viewLeft" android:layout_width="0dp" android:layout_height="0dp" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@id/viewMiddle" app:layout_constraintBottom_toTopOf="@id/storyReplyField" + app:layout_constraintEnd_toStartOf="@id/viewMiddle" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/storyAuthorProfilePicture" /> - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_post_creation.xml b/app/src/main/res/layout/fragment_post_creation.xml index 94317989..0ddafd95 100644 --- a/app/src/main/res/layout/fragment_post_creation.xml +++ b/app/src/main/res/layout/fragment_post_creation.xml @@ -11,7 +11,7 @@ android:id="@+id/carousel" android:layout_width="match_parent" android:layout_height="0dp" - app:showCaption="true" + app:showCaption="false" app:layout_constraintBottom_toTopOf="@+id/buttonConstraints" app:layout_constraintTop_toTopOf="parent"/> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37e8f5aa..f17aab39 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -337,4 +337,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Something went wrong fetching the carousel Sent reply Add Story + Error: could not mark story as seen + Start or pause the stories diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 56ca870c..63b1db63 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -23,6 +23,7 @@ - - - - - - - - - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b6a964cc..a2d1c457 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -10,11 +10,9 @@ @drawable/mascot_small - - - @style/BaseAppTheme.NoActionBar + @style/BaseAppTheme false true @@ -49,179 +47,4 @@ @android:color/transparent @android:color/transparent - - - - - - - - - - - - - - - - - - - - - - - - From f16f1a9927b752c397872c18f80b08b09ea7a0b7 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 13:29:27 +0100 Subject: [PATCH 22/46] Remove unused gps permission --- app/src/main/AndroidManifest.xml | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0ca1cd23..a975c2a3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,8 +12,6 @@ - - From 59e29ef2325e3ac81ac733036fbbe1d99001384c Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 13:32:46 +0100 Subject: [PATCH 23/46] Clarify Story API --- .../main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt index ed23d792..e9377dab 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/PixelfedAPI.kt @@ -165,6 +165,7 @@ interface PixelfedAPI { @Field("poll[expires_in]") poll_expires: List? = null, @Field("poll[multiple]") poll_multiple: List? = null, @Field("poll[hide_totals]") poll_hideTotals: List? = null, + //FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works @Field("sensitive") sensitive: Int? = null, @Field("spoiler_text") spoiler_text: String? = null, @Field("visibility") visibility: String = "public", @@ -253,14 +254,14 @@ interface PixelfedAPI { @POST("/api/v1.1/stories/add") fun storyUpload( @Part file: MultipartBody.Part, + // The API takes this value but then overwrites it in /api/v1.1/stories/publish, so ignore this @Part duration: MultipartBody.Part? = null, ): Observable @POST("/api/v1.1/stories/publish") suspend fun storyPublish( @Query("media_id") media_id: String, - //From 0 to 30 - //TODO figure out what this duration means + //From 0 to 30, duration in seconds of the story @Query("duration") duration: Int = 10, //FIXME this should be able to take a boolean or at least "true"/"false" but only "0"/"1" works. Same issue as sensitive boolean in postStatus @Query("can_reply") can_reply: String, From 4ac7aa6bcb015894053883f46251ba7299befb97 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 13:35:45 +0100 Subject: [PATCH 24/46] Avoid leaks of bindings --- .../org/pixeldroid/app/postCreation/PostCreationFragment.kt | 3 ++- .../org/pixeldroid/app/postCreation/PostSubmissionFragment.kt | 3 ++- .../org/pixeldroid/app/postCreation/camera/CameraFragment.kt | 3 ++- .../app/posts/feeds/cachedFeeds/CachedFeedFragment.kt | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt index 6490ba9e..c6a6b3d9 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt @@ -36,6 +36,7 @@ import org.pixeldroid.app.postCreation.camera.CameraActivity import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.postCreation.carousel.CarouselItem import org.pixeldroid.app.utils.BaseFragment +import org.pixeldroid.app.utils.bindingLifecycleAware import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.fileExtension @@ -53,7 +54,7 @@ class PostCreationFragment : BaseFragment() { private var user: UserDatabaseEntity? = null private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "") - private lateinit var binding: FragmentPostCreationBinding + private var binding: FragmentPostCreationBinding by bindingLifecycleAware() private lateinit var model: PostCreationViewModel override fun onCreateView( diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt index 2c1b68c8..0aa1821f 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt @@ -22,6 +22,7 @@ import org.pixeldroid.app.R import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.utils.BaseFragment +import org.pixeldroid.app.utils.bindingLifecycleAware import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.setSquareImageFromURL @@ -35,7 +36,7 @@ class PostSubmissionFragment : BaseFragment() { private var user: UserDatabaseEntity? = null private lateinit var instance: InstanceDatabaseEntity - private lateinit var binding: FragmentPostSubmissionBinding + private var binding: FragmentPostSubmissionBinding by bindingLifecycleAware() private lateinit var model: PostCreationViewModel override fun onCreateView( diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt index 00abfb97..4677d814 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt @@ -41,6 +41,7 @@ import org.pixeldroid.app.R import org.pixeldroid.app.databinding.FragmentCameraBinding import org.pixeldroid.app.postCreation.PostCreationActivity import org.pixeldroid.app.utils.BaseFragment +import org.pixeldroid.app.utils.bindingLifecycleAware import java.io.File import java.util.concurrent.ExecutorService import java.util.concurrent.Executors @@ -61,7 +62,7 @@ class CameraFragment : BaseFragment() { private val cameraLifecycleOwner = CameraLifecycleOwner() - private lateinit var binding: FragmentCameraBinding + private var binding: FragmentCameraBinding by bindingLifecycleAware() private var displayId: Int = -1 private var lensFacing: Int = CameraSelector.LENS_FACING_BACK diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt index 81be3ba0..4aa9abea 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/CachedFeedFragment.kt @@ -21,6 +21,7 @@ import org.pixeldroid.app.posts.feeds.initAdapter import org.pixeldroid.app.stories.StoriesAdapter import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.api.objects.FeedContentDatabase +import org.pixeldroid.app.utils.bindingLifecycleAware import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao import org.pixeldroid.app.utils.limitedLengthSmoothScrollToPosition @@ -34,7 +35,7 @@ open class CachedFeedFragment : BaseFragment() { internal lateinit var adapter: PagingDataAdapter internal var headerAdapter: StoriesAdapter? = null - private lateinit var binding: FragmentFeedBinding + private var binding: FragmentFeedBinding by bindingLifecycleAware() private var job: Job? = null From f2c1ae394236889e11f753e0c2788f5ea2a3625c Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 13:36:53 +0100 Subject: [PATCH 25/46] Remove duplicate values that are in pixel_common --- app/src/main/res/values/colors.xml | 243 ----------------------------- 1 file changed, 243 deletions(-) diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 34455d44..ccc257f8 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,247 +7,4 @@ #FFFFFF #b3b3b3 - - - #f4a261 - #924C00 - #FFFFFF - #FFDCC4 - #2F1400 - #745945 - #FFFFFF - #FFDCC4 - #2A1707 - #5D6136 - #FFFFFF - #E3E7AF - #1A1D00 - #BA1A1A - #FFDAD6 - #FFFFFF - #410002 - #FFFBFF - #201A17 - #FFFBFF - #201A17 - #F3DFD2 - #52443B - #84746A - #FBEEE8 - #362F2B - #FFB780 - #000000 - #924C00 - #924C00 - - #FFB780 - #4E2600 - #6F3800 - #FFDCC4 - #E4BFA7 - #422B1A - #5B412F - #FFDCC4 - #C6CA95 - #2F330C - #464A20 - #E3E7AF - #FFB4AB - #93000A - #690005 - #FFDAD6 - #201A17 - #ECE0DA - #201A17 - #ECE0DA - #52443B - #D6C3B7 - #9F8D82 - #201A17 - #ECE0DA - #924C00 - #000000 - #FFB780 - #FFB780 - - - #4285F4 - #005AC1 - #FFFFFF - #D8E2FF - #001A41 - #535E78 - #FFFFFF - #D8E2FF - #0F1B32 - #76517B - #FFFFFF - #FED6FF - #2D0E34 - #BA1A1A - #FFFFFF - #FFDAD6 - #410002 - #FEFBFF - #1B1B1F - #FEFBFF - #1B1B1F - #E1E2EC - #44474F - #74777F - #000000 - #303033 - #F2F0F4 - #ADC6FF - #005AC1 - #005AC1 - #ADC6FF - #002E69 - #004494 - #D8E2FF - #BBC6E4 - #253048 - #3B475F - #D8E2FF - #E5B8E8 - #44244A - #5D3A62 - #FED6FF - #FFB4AB - #690005 - #93000A - #FFB4AB - #1B1B1F - #E3E2E6 - #1B1B1F - #E3E2E6 - #44474F - #C4C6D0 - #8E9099 - #000000 - #E3E2E6 - #303033 - #005AC1 - #ADC6FF - #ADC6FF - - #86d89e - #006D3A - #FFFFFF - #99F6B5 - #00210E - #4F6353 - #FFFFFF - #D2E8D4 - #0D1F13 - #3A646F - #FFFFFF - #BEEAF6 - #001F26 - #BA1A1A - #FFDAD6 - #FFFFFF - #410002 - #FBFDF8 - #191C19 - #FBFDF8 - #191C19 - #DDE5DB - #414941 - #717971 - #F0F1EC - #2E312E - #7DDA9A - #000000 - #006D3A - #006D3A - #7DDA9A - #00391C - #00522B - #99F6B5 - #B6CCB8 - #223527 - #384B3C - #D2E8D4 - #A2CEDA - #02363F - #214C57 - #BEEAF6 - #FFB4AB - #93000A - #690005 - #FFDAD6 - #191C19 - #E1E3DE - #191C19 - #E1E3DE - #414941 - #C1C9BF - #8B938A - #191C19 - #E1E3DE - #006D3A - #000000 - #7DDA9A - #7DDA9A - - #984061 - #984061 - #FFFFFF - #FFD9E2 - #3E001D - #74565F - #FFFFFF - #FFD9E2 - #2B151C - #7C5635 - #FFFFFF - #FFDCC2 - #2E1500 - #BA1A1A - #FFDAD6 - #FFFFFF - #410002 - #FFFBFF - #201A1B - #FFFBFF - #201A1B - #F2DDE2 - #514347 - #837377 - #FAEEEF - #352F30 - #FFB0C8 - #000000 - #984061 - #984061 - #FFB0C8 - #5E1133 - #7B2949 - #FFD9E2 - #E2BDC6 - #422931 - #5A3F47 - #FFD9E2 - #EFBD94 - #48290C - #623F20 - #FFDCC2 - #FFB4AB - #93000A - #690005 - #FFDAD6 - #201A1B - #EBE0E1 - #201A1B - #EBE0E1 - #514347 - #D5C2C6 - #9E8C90 - #201A1B - #EBE0E1 - #984061 - #000000 - #FFB0C8 - #FFB0C8 From 3fdb7762b69cd17a079f295138a77584f74ce701 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 13:47:22 +0100 Subject: [PATCH 26/46] Use fancier Story progress bars --- .../pixeldroid/app/stories/StoriesActivity.kt | 17 +++-- .../pixeldroid/app/stories/StoryProgress.kt | 72 +++++++++++++++++++ app/src/main/res/layout/activity_stories.xml | 34 ++++----- app/src/main/res/values/strings.xml | 1 - 4 files changed, 92 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/org/pixeldroid/app/stories/StoryProgress.kt diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt index b4fecd1a..6fe175e9 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -39,6 +39,8 @@ class StoriesActivity: BaseActivity() { private lateinit var binding: ActivityStoriesBinding + private lateinit var storyProgress: StoryProgress + private lateinit var model: StoriesViewModel override fun onCreate(savedInstanceState: Bundle?) { @@ -58,6 +60,9 @@ class StoriesActivity: BaseActivity() { } model = _model + storyProgress = StoryProgress(model.uiState.value.imageList.size) + binding.storyProgressImage.setImageDrawable(storyProgress) + lifecycleScope.launch { lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { model.uiState.collect { uiState -> @@ -91,8 +96,7 @@ class StoriesActivity: BaseActivity() { binding.storyAuthor.text = uiState.username - binding.carouselProgress.text = getString(R.string.storyProgress) - .format(uiState.currentImage + 1, uiState.imageList.size) + storyProgress.currentStory = uiState.currentImage uiState.imageList.getOrNull(uiState.currentImage)?.let { Glide.with(binding.storyImage) @@ -156,13 +160,8 @@ class StoriesActivity: BaseActivity() { model.count.observe(this) { state -> // Render state in UI model.uiState.value.durationList.getOrNull(model.uiState.value.currentImage)?.let { - val percent = 100 - ((state/it.toFloat())*100).toInt() - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - binding.progressBarStory.setProgress(percent, true) - } else { - binding.progressBarStory.progress = percent - } + storyProgress.progress = 1 - (state/it.toFloat()) + binding.storyProgressImage.postInvalidate() } } diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoryProgress.kt b/app/src/main/java/org/pixeldroid/app/stories/StoryProgress.kt new file mode 100644 index 00000000..ebb2e2ba --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/stories/StoryProgress.kt @@ -0,0 +1,72 @@ +package org.pixeldroid.app.stories + +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.drawable.Drawable + +/** + * Copied & adapted from AntennaPod's EchoProgress class because it looked great and is very simple + * AntennaPod/ui/echo/src/main/java/de/danoeh/antennapod/ui/echo/EchoProgress.java + */ +class StoryProgress(private val numStories: Int) : Drawable() { + private val paint: Paint = Paint().apply { + flags = Paint.ANTI_ALIAS_FLAG + style = Paint.Style.STROKE + strokeJoin = Paint.Join.ROUND + strokeCap = Paint.Cap.ROUND + color = -0x1 + } + + var progress = 0f + var currentStory: Int = 0 + + override fun draw(canvas: Canvas) { + paint.strokeWidth = 0.5f * bounds.height() + val y = 0.5f * bounds.height() + val sectionWidth = 1.0f * bounds.width() / numStories + val sectionPadding = 0.03f * sectionWidth + // Iterate over stories + for (i in 0 until numStories) { + if (i < currentStory) { + // If current drawing position is smaller than current story, the paint we will use + // should be opaque: this story is already "seen" + paint.alpha = 255 + } else { + // Otherwise it should be somewhat transparent, denoting it is not yet seen + paint.alpha = 100 + } + // Draw an entire line with the paint, for now ignoring partial progress within the + // current story + canvas.drawLine( + i * sectionWidth + sectionPadding, + y, + (i + 1) * sectionWidth - sectionPadding, + y, + paint + ) + // If current position is equal to progress, we are drawing the current story. Thus we + // should account for partial progress and paint the beginning of the line opaquely + if (i == currentStory) { + paint.alpha = 255 + canvas.drawLine( + currentStory * sectionWidth + sectionPadding, + y, + currentStory * sectionWidth + sectionPadding + progress * (sectionWidth - 2 * sectionPadding), + y, + paint + ) + } + } + } + + @Deprecated("Deprecated in Java") + override fun getOpacity(): Int { + return PixelFormat.TRANSLUCENT + } + + override fun setAlpha(alpha: Int) {} + override fun setColorFilter(cf: ColorFilter?) {} +} + diff --git a/app/src/main/res/layout/activity_stories.xml b/app/src/main/res/layout/activity_stories.xml index 957891e1..a0c58001 100644 --- a/app/src/main/res/layout/activity_stories.xml +++ b/app/src/main/res/layout/activity_stories.xml @@ -58,7 +58,7 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/progressBarStory" + app:layout_constraintTop_toBottomOf="@id/story_progress_image" app:layout_constraintVertical_bias="1.0" tools:scaleType="centerCrop" tools:srcCompat="@tools:sample/backgrounds/scenic[10]" /> @@ -73,21 +73,21 @@ android:src="@drawable/play_pause" app:layout_constraintBottom_toBottomOf="@+id/storyAuthor" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/carouselProgress" + app:layout_constraintTop_toTopOf="@+id/storyAge" tools:visibility="visible" /> - - + app:layout_constraintTop_toTopOf="parent" /> - - NSFW/CW posts will not be blurred, and will be shown by default. Story image Reply to %1$s - %1$s / %2$s Something went wrong sending reply Something went wrong fetching the carousel Sent reply From 4008d2a2fcfe699b96e13f45d62ab01fb8c59960 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 13:52:51 +0100 Subject: [PATCH 27/46] Update dependencies --- app/build.gradle | 4 ++-- .../java/org/pixeldroid/app/stories/StoriesActivity.kt | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a242fa2b..c3ec95a5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -197,7 +197,7 @@ dependencies { implementation project(path: ':scrambler') implementation project(path: ':pixel_common') - implementation('com.github.bumptech.glide:glide:4.14.2') { + implementation('com.github.bumptech.glide:glide:4.16.0') { exclude group: "com.android.support" } @@ -206,7 +206,7 @@ dependencies { // Excludes the support library because it's already included by Glide. transitive = false } - implementation 'com.github.bumptech.glide:annotations:4.14.2' + implementation 'com.github.bumptech.glide:annotations:4.16.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2' ksp 'com.github.bumptech.glide:ksp:4.14.2' diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt index 6fe175e9..164c4030 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -105,15 +105,15 @@ class StoriesActivity: BaseActivity() { override fun onLoadFailed( e: GlideException?, model: Any?, - target: Target?, + target: Target, isFirstResource: Boolean, ): Boolean = false override fun onResourceReady( - resource: Drawable?, - m: Any?, + resource: Drawable, + model: Any, target: Target?, - dataSource: DataSource?, + dataSource: DataSource, isFirstResource: Boolean, ): Boolean { Glide.with(binding.storyImage) From 0290e6f8d5ea8fb6c45454fb0d6a858f88bc57a9 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 13:55:24 +0100 Subject: [PATCH 28/46] Update pixel_common --- pixel_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixel_common b/pixel_common index 051e4470..c43d9bba 160000 --- a/pixel_common +++ b/pixel_common @@ -1 +1 @@ -Subproject commit 051e44705dc3711f138b96935985b06e8e1c2ef3 +Subproject commit c43d9bbad12ddbd99f943375a1ae4be3dff70a5e From bb3c9afb13bd1ea1e3b5ab65a7704cdf6ede5c75 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 14:03:36 +0100 Subject: [PATCH 29/46] Add Self view for stories --- .../pixeldroid/app/stories/StoriesActivity.kt | 8 +- .../app/stories/StoriesViewModel.kt | 41 ++++--- .../app/stories/StoryCarouselViewHolder.kt | 74 +++++++++---- .../app/utils/api/objects/StoryCarousel.kt | 9 +- app/src/main/res/drawable/story_play.xml | 5 + .../res/layout/story_carousel_add_story.xml | 46 -------- .../main/res/layout/story_carousel_self.xml | 102 ++++++++++++++++++ app/src/main/res/values/strings.xml | 8 ++ 8 files changed, 208 insertions(+), 85 deletions(-) create mode 100644 app/src/main/res/drawable/story_play.xml delete mode 100644 app/src/main/res/layout/story_carousel_add_story.xml create mode 100644 app/src/main/res/layout/story_carousel_self.xml diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt index 164c4030..8ca480e5 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -1,7 +1,6 @@ package org.pixeldroid.app.stories import android.graphics.drawable.Drawable -import android.os.Build import android.os.Bundle import android.view.MotionEvent import android.view.View.OnClickListener @@ -26,6 +25,7 @@ import org.pixeldroid.app.databinding.ActivityStoriesBinding import org.pixeldroid.app.posts.setTextViewFromISO8601 import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.api.objects.Account +import org.pixeldroid.app.utils.api.objects.Story import org.pixeldroid.app.utils.api.objects.StoryCarousel @@ -33,6 +33,7 @@ class StoriesActivity: BaseActivity() { companion object { const val STORY_CAROUSEL = "LaunchStoryCarousel" + const val STORY_CAROUSEL_SELF = "LaunchStoryCarouselSelf" const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId" } @@ -49,14 +50,15 @@ class StoriesActivity: BaseActivity() { super.onCreate(savedInstanceState) - val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as StoryCarousel + val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as? StoryCarousel val userId = intent.getStringExtra(STORY_CAROUSEL_USER_ID) + val selfCarousel: Array? = intent.getSerializableExtra(STORY_CAROUSEL_SELF) as? Array binding = ActivityStoriesBinding.inflate(layoutInflater) setContentView(binding.root) val _model: StoriesViewModel by viewModels { - StoriesViewModelFactory(application, carousel, userId) + StoriesViewModelFactory(application, carousel, userId, selfCarousel?.asList()) } model = _model diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt index 733ef3c1..0d42af37 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt @@ -4,21 +4,21 @@ import android.app.Application import android.os.CountDownTimer import android.text.Editable import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.os.LocaleListCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.pixeldroid.app.R import org.pixeldroid.app.utils.PixelDroidApplication +import org.pixeldroid.app.utils.api.objects.CarouselUserContainer +import org.pixeldroid.app.utils.api.objects.Story import org.pixeldroid.app.utils.api.objects.StoryCarousel +import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.di.PixelfedAPIHolder import java.time.Instant import javax.inject.Inject @@ -40,20 +40,21 @@ data class StoriesUiState( class StoriesViewModel( application: Application, - val carousel: StoryCarousel, - userId: String? + val carousel: StoryCarousel?, + userId: String?, + val selfCarousel: List? ) : AndroidViewModel(application) { @Inject lateinit var apiHolder: PixelfedAPIHolder + @Inject + lateinit var db: AppDatabase - private var currentAccount = carousel.nodes?.firstOrNull { it?.user?.id == userId } + private var currentAccount: CarouselUserContainer? - private val _uiState: MutableStateFlow = MutableStateFlow( - newUiStateFromCurrentAccount() - ) + private val _uiState: MutableStateFlow - val uiState: StateFlow = _uiState + val uiState: StateFlow val count = MutableLiveData() @@ -61,12 +62,20 @@ class StoriesViewModel( init { (application as PixelDroidApplication).getAppComponent().inject(this) + currentAccount = + if (selfCarousel != null) { + db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel) } + } else carousel?.nodes?.firstOrNull { it?.user?.id == userId } + + _uiState = MutableStateFlow(newUiStateFromCurrentAccount()) + uiState = _uiState + startTimerForCurrent() } private fun setTimer(timerLength: Float) { count.value = timerLength - timer = object: CountDownTimer((timerLength * 1000).toLong(), 100){ + timer = object: CountDownTimer((timerLength * 1000).toLong(), 50){ override fun onTick(millisUntilFinished: Long) { count.value = millisUntilFinished.toFloat() / 1000 @@ -98,8 +107,9 @@ class StoriesViewModel( ) } } else { + if(selfCarousel != null) return val currentUserId = currentAccount?.user?.id - val currentAccountIndex = carousel.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return + val currentAccountIndex = carousel?.nodes?.indexOfFirst { it?.user?.id == currentUserId } ?: return currentAccount = when (index) { uiState.value.imageList.size -> { // Go to next user @@ -209,10 +219,11 @@ class StoriesViewModel( class StoriesViewModelFactory( val application: Application, - val carousel: StoryCarousel, - val userId: String? + val carousel: StoryCarousel?, + val userId: String?, + val selfCarousel: List? ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java).newInstance(application, carousel, userId) + return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java, List::class.java).newInstance(application, carousel, userId, selfCarousel) } } diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt b/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt index 5d884dfa..ba0bcbd9 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoryCarouselViewHolder.kt @@ -2,26 +2,33 @@ package org.pixeldroid.app.stories import android.annotation.SuppressLint import android.content.Intent +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.RenderEffect +import android.graphics.Shader +import android.os.Build import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.lifecycle.LifecycleCoroutineScope -import androidx.paging.LoadState import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import kotlinx.coroutines.launch import org.pixeldroid.app.R -import org.pixeldroid.app.databinding.StoryCarouselAddStoryBinding import org.pixeldroid.app.databinding.StoryCarouselBinding import org.pixeldroid.app.databinding.StoryCarouselItemBinding +import org.pixeldroid.app.databinding.StoryCarouselSelfBinding import org.pixeldroid.app.postCreation.camera.CameraActivity import org.pixeldroid.app.postCreation.camera.CameraFragment import org.pixeldroid.app.utils.api.objects.CarouselUserContainer +import org.pixeldroid.app.utils.api.objects.Story import org.pixeldroid.app.utils.api.objects.StoryCarousel import org.pixeldroid.app.utils.di.PixelfedAPIHolder /** - * Adapter to the show the a [RecyclerView] item for a [LoadState] + * Adapter that has either 1 or 0 items, to show stories widget or not */ class StoriesAdapter(val lifecycleScope: LifecycleCoroutineScope, val apiHolder: PixelfedAPIHolder) : RecyclerView.Adapter() { var carousel: StoryCarousel? = null @@ -74,7 +81,8 @@ class StoriesAdapter(val lifecycleScope: LifecycleCoroutineScope, val apiHolder: val api = apiHolder.api ?: apiHolder.setToCurrentUser() val carousel = api.carousel() - if (carousel.nodes?.isEmpty() != true) { + // If there are stories from someone else or our stories to show, show them + if (carousel.nodes?.isEmpty() == false || carousel.self?.nodes?.isEmpty() == false) { // Pass carousel to adapter gotStories(carousel) } else { @@ -113,8 +121,12 @@ class StoriesListAdapter : RecyclerView.Adapter() { private var storyCarousel: StoryCarousel? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return if(viewType == R.layout.story_carousel_add_story){ - val v = StoryCarouselAddStoryBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return if(viewType == R.layout.story_carousel_self){ + val v = StoryCarouselSelfBinding.inflate(LayoutInflater.from(parent.context), parent, false) + v.myStory.visibility = + if (storyCarousel?.self?.nodes?.isEmpty() == false) View.VISIBLE + else View.GONE + AddViewHolder(v) } else { @@ -124,7 +136,7 @@ class StoriesListAdapter : RecyclerView.Adapter() { } override fun getItemViewType(position: Int): Int { - return if(position == 0) R.layout.story_carousel_add_story + return if(position == 0) R.layout.story_carousel_self else R.layout.story_carousel_item } @@ -133,21 +145,15 @@ class StoriesListAdapter : RecyclerView.Adapter() { val carouselPosition = position - 1 storyCarousel?.nodes?.get(carouselPosition)?.let { (holder as ViewHolder).bindItem(it) } holder.itemView.setOnClickListener { - storyCarousel?.let { carousel -> - storyCarousel?.nodes?.get(carouselPosition)?.user?.id?.let { userId -> - val intent = Intent(holder.itemView.context, StoriesActivity::class.java) - intent.putExtra(StoriesActivity.STORY_CAROUSEL, carousel) - intent.putExtra(StoriesActivity.STORY_CAROUSEL_USER_ID, userId) - holder.itemView.context.startActivity(intent) - } + storyCarousel?.nodes?.get(carouselPosition)?.user?.id?.let { userId -> + val intent = Intent(holder.itemView.context, StoriesActivity::class.java) + intent.putExtra(StoriesActivity.STORY_CAROUSEL, storyCarousel) + intent.putExtra(StoriesActivity.STORY_CAROUSEL_USER_ID, userId) + holder.itemView.context.startActivity(intent) } } } else { - holder.itemView.setOnClickListener { - val intent = Intent(holder.itemView.context, CameraActivity::class.java) - intent.putExtra(CameraFragment.CAMERA_ACTIVITY_STORY, true) - holder.itemView.context.startActivity(intent) - } + storyCarousel?.self?.nodes?.let { (holder as? AddViewHolder)?.bindItem(it.filterNotNull()) } } } @@ -162,7 +168,35 @@ class StoriesListAdapter : RecyclerView.Adapter() { notifyDataSetChanged() } - class AddViewHolder(itemBinding: StoryCarouselAddStoryBinding) : RecyclerView.ViewHolder(itemBinding.root) + class AddViewHolder(private val itemBinding: StoryCarouselSelfBinding) : RecyclerView.ViewHolder(itemBinding.root) { + fun bindItem(nodes: List) { + itemBinding.addStory.setOnClickListener { + val intent = Intent(itemView.context, CameraActivity::class.java) + intent.putExtra(CameraFragment.CAMERA_ACTIVITY_STORY, true) + itemView.context.startActivity(intent) + } + itemBinding.myStory.setOnClickListener { + val intent = Intent(itemView.context, StoriesActivity::class.java) + intent.putExtra(StoriesActivity.STORY_CAROUSEL_SELF, nodes.toTypedArray()) + itemView.context.startActivity(intent) + } + + // Only show image on new Android versions, because the transformations need it and the + // text is not legible without the transformations + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Glide.with(itemBinding.root).load(nodes.firstOrNull()?.src).into(itemBinding.carouselImageView) + val value = 70 * 255 / 100 + val darkFilterRenderEffect = PorterDuffColorFilter(Color.argb(value, 0, 0, 0), PorterDuff.Mode.SRC_ATOP) + val blurRenderEffect = + RenderEffect.createBlurEffect( + 4f, 4f, Shader.TileMode.MIRROR + ) + val combinedEffect = RenderEffect.createColorFilterEffect(darkFilterRenderEffect, blurRenderEffect) + itemBinding.carouselImageView.setRenderEffect(combinedEffect) + } + + } + } class ViewHolder(private val itemBinding: StoryCarouselItemBinding) : RecyclerView.ViewHolder(itemBinding.root) { diff --git a/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt index 73c22665..bf3a7395 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/api/objects/StoryCarousel.kt @@ -1,5 +1,6 @@ package org.pixeldroid.app.utils.api.objects +import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import java.io.Serializable import java.time.Instant @@ -23,7 +24,13 @@ data class CarouselUser( data class CarouselUserContainer( val user: CarouselUser?, val nodes: List?, -): Serializable +): Serializable { + constructor(user: UserDatabaseEntity, nodes: List?) : this( + CarouselUser(user.user_id, user.username, null, user.avatar_static, + local = true, + is_author = true + ), nodes) +} data class Story( val id: String?, diff --git a/app/src/main/res/drawable/story_play.xml b/app/src/main/res/drawable/story_play.xml new file mode 100644 index 00000000..476e56f7 --- /dev/null +++ b/app/src/main/res/drawable/story_play.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/story_carousel_add_story.xml b/app/src/main/res/layout/story_carousel_add_story.xml deleted file mode 100644 index 9b0e82c9..00000000 --- a/app/src/main/res/layout/story_carousel_add_story.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/layout/story_carousel_self.xml b/app/src/main/res/layout/story_carousel_self.xml new file mode 100644 index 00000000..eb963e88 --- /dev/null +++ b/app/src/main/res/layout/story_carousel_self.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2aa9d37e..d154a3a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -338,4 +338,12 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Add Story Error: could not mark story as seen Start or pause the stories + My story + + Story + + Post + Continue + Pictures after the first were removed but can be restored by switching back to creating a Post + Story Duration From 8703287d909e0ca60395af354f2e822a5940651d Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Thu, 21 Dec 2023 14:06:20 +0100 Subject: [PATCH 30/46] Story creation integration --- .../app/postCreation/PostCreationFragment.kt | 51 ++++++-- .../app/postCreation/PostCreationViewModel.kt | 118 +++++++++++++----- .../postCreation/PostSubmissionFragment.kt | 28 ++++- .../app/postCreation/camera/CameraFragment.kt | 3 +- app/src/main/res/drawable/arrow_forward.xml | 5 + .../res/layout/fragment_post_creation.xml | 87 +++++++++---- .../res/layout/fragment_post_submission.xml | 83 +++++++++++- 7 files changed, 301 insertions(+), 74 deletions(-) create mode 100644 app/src/main/res/drawable/arrow_forward.xml diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt index c6a6b3d9..44d6da62 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt @@ -65,6 +65,7 @@ class PostCreationFragment : BaseFragment() { // Inflate the layout for this fragment binding = FragmentPostCreationBinding.inflate(layoutInflater) + return binding.root } @@ -91,11 +92,6 @@ class PostCreationFragment : BaseFragment() { } model = _model - if(model.storyCreation){ - binding.carousel.showCaption = false - //TODO hide grid button, hide dot (indicator), hide arrows, limit photos to 1 - } - model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData -> // update UI binding.carousel.addData( @@ -107,6 +103,7 @@ class PostCreationFragment : BaseFragment() { ) } ) + binding.postCreationNextButton.isEnabled = newPhotoData.isNotEmpty() } lifecycleScope.launch { @@ -127,13 +124,26 @@ class PostCreationFragment : BaseFragment() { binding.toolbarPostCreation.visibility = if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE binding.carousel.layoutCarousel = uiState.isCarousel + + if(uiState.storyCreation){ + binding.toggleStoryPost.check(binding.buttonStory.id) + binding.buttonStory.isPressed = true + binding.carousel.showLayoutSwitchButton = false + binding.carousel.showIndicator = false + } else { + binding.toggleStoryPost.check(binding.buttonPost.id) + binding.carousel.showLayoutSwitchButton = true + binding.carousel.showIndicator = true + } + binding.carousel.maxEntries = uiState.maxEntries + } } } binding.carousel.apply { layoutCarouselCallback = { model.becameCarousel(it)} - maxEntries = instance.albumLimit + maxEntries = if(model.uiState.value.storyCreation) 1 else instance.albumLimit addPhotoButtonCallback = { addPhoto() } @@ -141,9 +151,10 @@ class PostCreationFragment : BaseFragment() { model.updateDescription(position, description) } } - // get the description and send the post - binding.postCreationSendButton.setOnClickListener { - if (validatePost() && model.isNotEmpty()) { + + // Validate the post and go to the next step of the post creation process + binding.postCreationNextButton.setOnClickListener { + if (validatePost()) { findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment) } } @@ -171,6 +182,23 @@ class PostCreationFragment : BaseFragment() { } } + binding.toggleStoryPost.addOnButtonCheckedListener { _, checkedId, isChecked -> + // Only handle checked events + if (!isChecked) return@addOnButtonCheckedListener + + when (checkedId) { + R.id.buttonStory -> { + model.storyMode(true) + } + R.id.buttonPost -> { + model.storyMode(false) + } + } + + } + + binding.backbutton.setOnClickListener{requireActivity().onBackPressedDispatcher.onBackPressed()} + // Clean up temporary files, if any val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES) tempFiles?.asList()?.forEach { @@ -284,8 +312,9 @@ class PostCreationFragment : BaseFragment() { private fun validatePost(): Boolean { if (model.getPhotoData().value?.none { it.video && it.videoEncodeComplete == false } == true) { - // Encoding is done, i.e. none of the items are both a video and not done encoding - return true + // Encoding is done, i.e. none of the items are both a video and not done encoding. + // We return true if the post is not empty, false otherwise. + return model.getPhotoData().value?.isNotEmpty() == true } // Encoding is not done, show a dialog and return false to indicate validation failed MaterialAlertDialogBuilder(requireActivity()).apply { diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index 532f118e..e6f0cc54 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -55,7 +55,6 @@ import kotlin.collections.forEach import kotlin.collections.get import kotlin.collections.getOrNull import kotlin.collections.indexOfFirst -import kotlin.collections.isNotEmpty import kotlin.collections.mutableListOf import kotlin.collections.mutableMapOf import kotlin.collections.plus @@ -71,6 +70,7 @@ data class PostCreationActivityUiState( val addPhotoButtonEnabled: Boolean = true, val editPhotoButtonEnabled: Boolean = true, val removePhotoButtonEnabled: Boolean = true, + val maxEntries: Int?, val isCarousel: Boolean = true, @@ -87,6 +87,11 @@ data class PostCreationActivityUiState( val uploadErrorVisible: Boolean = false, val uploadErrorExplanationText: String = "", val uploadErrorExplanationVisible: Boolean = false, + + val storyCreation: Boolean, + val storyDuration: Int = 10, + val storyReplies: Boolean = true, + val storyReactions: Boolean = true, ) @Parcelize @@ -109,8 +114,9 @@ class PostCreationViewModel( val instance: InstanceDatabaseEntity? = null, existingDescription: String? = null, existingNSFW: Boolean = false, - val storyCreation: Boolean = false, + storyCreation: Boolean = false, ) : AndroidViewModel(application) { + private var storyPhotoDataBackup: MutableList? = null private val photoData: MutableLiveData> by lazy { MutableLiveData>().also { it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) } @@ -130,7 +136,9 @@ class PostCreationViewModel( _uiState = MutableStateFlow(PostCreationActivityUiState( newPostDescriptionText = existingDescription ?: templateDescription, - nsfw = existingNSFW + nsfw = existingNSFW, + maxEntries = if(storyCreation) 1 else instance?.albumLimit, + storyCreation = storyCreation )) } @@ -147,35 +155,41 @@ class PostCreationViewModel( } } + /** + * Read-only public view on [photoData] + */ fun getPhotoData(): LiveData> = photoData /** * Will add as many images as possible to [photoData], from the [clipData], and if - * ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images + * ([photoData].size + [clipData].itemCount) > uiState.value.maxEntries then it will only add as many images * as are legal (if any) and a dialog will be shown to the user alerting them of this fact. */ fun addPossibleImages(clipData: ClipData, previousList: MutableList? = photoData.value): MutableList { val dataToAdd: ArrayList = arrayListOf() var count = clipData.itemCount - if(count + (previousList?.size ?: 0) > instance!!.albumLimit){ - _uiState.update { currentUiState -> - currentUiState.copy(userMessage = getApplication().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit)) + uiState.value.maxEntries?.let { + if(count + (previousList?.size ?: 0) > it){ + _uiState.update { currentUiState -> + currentUiState.copy(userMessage = getApplication().getString(R.string.total_exceeds_album_limit).format(it)) + } + count = count.coerceAtMost(it - (previousList?.size ?: 0)) } - count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0)) - } - if (count + (previousList?.size ?: 0) >= instance.albumLimit) { - // Disable buttons to add more images - _uiState.update { currentUiState -> - currentUiState.copy(addPhotoButtonEnabled = false) - } - } - for (i in 0 until count) { - clipData.getItemAt(i).let { - val sizeAndVideoPair: Pair = - getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1) - dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString())) + if (count + (previousList?.size ?: 0) >= it) { + // Disable buttons to add more images + _uiState.update { currentUiState -> + currentUiState.copy(addPhotoButtonEnabled = false) + } + } + for (i in 0 until count) { + clipData.getItemAt(i).let { + val sizeAndVideoPair: Pair = + getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1) + dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString())) + } } } + return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf() } @@ -187,15 +201,15 @@ class PostCreationViewModel( * Returns the size of the file of the Uri, and whether it is a video, * and opens a dialog in case it is too big or in case the file is unsupported. */ - fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair { + private fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair { val size: Long = if (uri.scheme =="content") { getApplication().contentResolver.query(uri, null, null, null, null) ?.use { cursor -> /* Get the column indexes of the data in the Cursor, - * move to the first row in the Cursor, get the data, - * and display it. - */ + * move to the first row in the Cursor, get the data, + * and display it. + */ val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) if(sizeIndex >= 0) { cursor.moveToFirst() @@ -217,6 +231,7 @@ class PostCreationViewModel( } if ((!isVideo && sizeInkBytes > instance!!.maxPhotoSize) || (isVideo && sizeInkBytes > instance!!.maxVideoSize)) { + //TODO Offer remedy for too big file: re-compress it val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize _uiState.update { currentUiState -> currentUiState.copy( @@ -227,8 +242,6 @@ class PostCreationViewModel( return Pair(size, isVideo) } - fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false - fun updateDescription(position: Int, description: String) { photoData.value?.getOrNull(position)?.imageDescription = description photoData.value = photoData.value @@ -238,8 +251,8 @@ class PostCreationViewModel( photoData.value?.removeAt(currentPosition) _uiState.update { it.copy( - addPhotoButtonEnabled = true - ) + addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (uiState.value.maxEntries ?: 0), + ) } photoData.value = photoData.value } @@ -258,7 +271,7 @@ class PostCreationViewModel( videoEncodeProgress = 0 videoEncodeComplete = false - VideoEditActivity.startEncoding(imageUri, it, + VideoEditActivity.startEncoding(imageUri, null, it, context = getApplication(), registerNewFFmpegSession = ::registerNewFFmpegSession, trackTempFile = ::trackTempFile, @@ -447,9 +460,8 @@ class PostCreationViewModel( } ?: apiHolder.api ?: apiHolder.setToCurrentUser() val inter: Observable = - //TODO specify story duration //TODO validate that image is correct (?) aspect ratio - if (storyCreation) api.storyUpload(requestBody.parts[0]) + if (uiState.value.storyCreation) api.storyUpload(requestBody.parts[0]) else api.mediaUpload(description, requestBody.parts[0]) apiHolder.api = null @@ -459,7 +471,7 @@ class PostCreationViewModel( .subscribe( { attachment: Attachment -> data.progress = 0 - data.uploadId = if(storyCreation){ + data.uploadId = if(uiState.value.storyCreation){ attachment.media_id!! } else { attachment.id!! @@ -519,11 +531,11 @@ class PostCreationViewModel( apiHolder.setToCurrentUser(it) } ?: apiHolder.api ?: apiHolder.setToCurrentUser() - if(storyCreation){ + if(uiState.value.storyCreation){ api.storyPublish( media_id = getPhotoData().value!!.firstNotNullOf { it.uploadId }, can_react = "1", can_reply = "1", - duration = 10 + duration = uiState.value.storyDuration ) } else{ api.postStatus( @@ -571,6 +583,44 @@ class PostCreationViewModel( fun chooseAccount(which: UserDatabaseEntity) { _uiState.update { it.copy(chosenAccount = which) } } + + fun storyMode(storyMode: Boolean) { + //TODO check ratio of files in story mode? What is acceptable? + + val newMaxEntries = if (storyMode) 1 else instance?.albumLimit + var newUiState = _uiState.value.copy( + storyCreation = storyMode, + maxEntries = newMaxEntries, + addPhotoButtonEnabled = (photoData.value?.size ?: 0) < (newMaxEntries ?: 0), + ) + // If switching to story, and there are too many pictures, keep the first and backup the rest + if (storyMode && (photoData.value?.size ?: 0) > 1){ + storyPhotoDataBackup = photoData.value + + photoData.value = photoData.value?.let { mutableListOf(it.firstOrNull()).filterNotNull().toMutableList() } + + //Show message saying extraneous pictures were removed but can be restored + newUiState = newUiState.copy( + userMessage = getApplication().getString(R.string.extraneous_pictures_stories) + ) + } + // Restore if backup not null and first value is unchanged + else if (storyPhotoDataBackup != null && storyPhotoDataBackup?.firstOrNull() == photoData.value?.firstOrNull()){ + photoData.value = storyPhotoDataBackup + storyPhotoDataBackup = null + } + _uiState.update { newUiState } + } + + fun storyDuration(value: Int) { + _uiState.update { + it.copy(storyDuration = value) + } + } + + fun updateStoryReactions(checked: Boolean) { _uiState.update { it.copy(storyReactions = checked) } } + + fun updateStoryReplies(checked: Boolean) { _uiState.update { it.copy(storyReplies = checked) } } } class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean, val storyCreation: Boolean) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt index 0aa1821f..3fadc832 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt @@ -26,6 +26,7 @@ import org.pixeldroid.app.utils.bindingLifecycleAware import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.setSquareImageFromURL +import kotlin.math.roundToInt class PostSubmissionFragment : BaseFragment() { @@ -80,12 +81,16 @@ class PostSubmissionFragment : BaseFragment() { binding.nsfwSwitch.isChecked = model.uiState.value.nsfw binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText) - if(model.storyCreation){ + if(model.uiState.value.storyCreation){ binding.nsfwSwitch.visibility = View.GONE binding.postTextInputLayout.visibility = View.GONE binding.privateTitle.visibility = View.GONE binding.postPreview.visibility = View.GONE - //TODO show story specific stuff here + + binding.storyOptions.visibility = View.VISIBLE + binding.storyDurationSlider.value = model.uiState.value.storyDuration.toFloat() + binding.storyRepliesSwitch.isChecked = model.uiState.value.storyReplies + binding.storyReactionsSwitch.isChecked = model.uiState.value.storyReactions } lifecycleScope.launch { @@ -125,13 +130,24 @@ class PostSubmissionFragment : BaseFragment() { binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked -> model.updateNSFW(isChecked) } + binding.storyRepliesSwitch.setOnCheckedChangeListener { _, isChecked -> + model.updateStoryReplies(isChecked) + } + binding.storyReactionsSwitch.setOnCheckedChangeListener { _, isChecked -> + model.updateStoryReactions(isChecked) + } binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars + binding.storyDurationSlider.addOnChangeListener { _, value, _ -> + // Responds to when slider's value is changed + model.storyDuration(value.roundToInt()) + } + setSquareImageFromURL(View(requireActivity()), model.getPhotoData().value?.get(0)?.imageUri.toString(), binding.postPreview) // Get the description and send the post - binding.postCreationSendButton.setOnClickListener { + binding.postSubmissionSendButton.setOnClickListener { if (validatePost()) model.upload() } @@ -190,13 +206,13 @@ class PostSubmissionFragment : BaseFragment() { } private fun enableButton(enable: Boolean = true){ - binding.postCreationSendButton.isEnabled = enable + binding.postSubmissionSendButton.isEnabled = enable if(enable){ binding.postingProgressBar.visibility = View.GONE - binding.postCreationSendButton.visibility = View.VISIBLE + binding.postSubmissionSendButton.visibility = View.VISIBLE } else { binding.postingProgressBar.visibility = View.VISIBLE - binding.postCreationSendButton.visibility = View.GONE + binding.postSubmissionSendButton.visibility = View.GONE } } diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt index 4677d814..e7cb6ba4 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt @@ -340,7 +340,8 @@ class CameraFragment : BaseFragment() { putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) action = Intent.ACTION_GET_CONTENT addCategory(Intent.CATEGORY_OPENABLE) - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + // Don't allow multiple for story + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, !addToStory) uploadImageResultContract.launch( Intent.createChooser(this, null) ) diff --git a/app/src/main/res/drawable/arrow_forward.xml b/app/src/main/res/drawable/arrow_forward.xml new file mode 100644 index 00000000..23072282 --- /dev/null +++ b/app/src/main/res/drawable/arrow_forward.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/fragment_post_creation.xml b/app/src/main/res/layout/fragment_post_creation.xml index 94317989..13e11db2 100644 --- a/app/src/main/res/layout/fragment_post_creation.xml +++ b/app/src/main/res/layout/fragment_post_creation.xml @@ -11,29 +11,74 @@ android:id="@+id/carousel" android:layout_width="match_parent" android:layout_height="0dp" - app:showCaption="true" - app:layout_constraintBottom_toTopOf="@+id/buttonConstraints" - app:layout_constraintTop_toTopOf="parent"/> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@+id/top_bar" + app:showCaption="true" /> + android:minHeight="?attr/actionBarSize" + android:theme="?attr/actionBarTheme" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> -